[v1.x] fix: handle non-UTF-8 bytes in stdio server stdin (#2303)
This commit is contained in:
parent
f8d98b63a7
commit
2e9897e2b9
2 changed files with 35 additions and 2 deletions
|
|
@ -44,7 +44,7 @@ async def stdio_server(
|
||||||
# python is platform-dependent (Windows is particularly problematic), so we
|
# python is platform-dependent (Windows is particularly problematic), so we
|
||||||
# re-wrap the underlying binary stream to ensure UTF-8.
|
# re-wrap the underlying binary stream to ensure UTF-8.
|
||||||
if not stdin:
|
if not stdin:
|
||||||
stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8"))
|
stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace"))
|
||||||
if not stdout:
|
if not stdout:
|
||||||
stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8"))
|
stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8"))
|
||||||
|
|
||||||
|
|
@ -63,7 +63,7 @@ async def stdio_server(
|
||||||
async for line in stdin:
|
async for line in stdin:
|
||||||
try:
|
try:
|
||||||
message = types.JSONRPCMessage.model_validate_json(line)
|
message = types.JSONRPCMessage.model_validate_json(line)
|
||||||
except Exception as exc: # pragma: no cover
|
except Exception as exc:
|
||||||
await read_stream_writer.send(exc)
|
await read_stream_writer.send(exc)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import io
|
import io
|
||||||
|
import sys
|
||||||
|
from io import TextIOWrapper
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -59,3 +61,34 @@ async def test_stdio_server():
|
||||||
assert len(received_responses) == 2
|
assert len(received_responses) == 2
|
||||||
assert received_responses[0] == JSONRPCMessage(root=JSONRPCRequest(jsonrpc="2.0", id=3, method="ping"))
|
assert received_responses[0] == JSONRPCMessage(root=JSONRPCRequest(jsonrpc="2.0", id=3, method="ping"))
|
||||||
assert received_responses[1] == JSONRPCMessage(root=JSONRPCResponse(jsonrpc="2.0", id=4, result={}))
|
assert received_responses[1] == JSONRPCMessage(root=JSONRPCResponse(jsonrpc="2.0", id=4, result={}))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_stdio_server_invalid_utf8(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
"""Non-UTF-8 bytes on stdin must not crash the server.
|
||||||
|
|
||||||
|
Invalid bytes are replaced with U+FFFD, which then fails JSON parsing and
|
||||||
|
is delivered as an in-stream exception. Subsequent valid messages must
|
||||||
|
still be processed.
|
||||||
|
"""
|
||||||
|
# \xff\xfe are invalid UTF-8 start bytes.
|
||||||
|
valid = JSONRPCRequest(jsonrpc="2.0", id=1, method="ping")
|
||||||
|
raw_stdin = io.BytesIO(b"\xff\xfe\n" + valid.model_dump_json(by_alias=True, exclude_none=True).encode() + b"\n")
|
||||||
|
|
||||||
|
# Replace sys.stdin with a wrapper whose .buffer is our raw bytes, so that
|
||||||
|
# stdio_server()'s default path wraps it with errors='replace'.
|
||||||
|
monkeypatch.setattr(sys, "stdin", TextIOWrapper(raw_stdin, encoding="utf-8"))
|
||||||
|
monkeypatch.setattr(sys, "stdout", TextIOWrapper(io.BytesIO(), encoding="utf-8"))
|
||||||
|
|
||||||
|
with anyio.fail_after(5):
|
||||||
|
async with stdio_server() as (read_stream, write_stream):
|
||||||
|
await write_stream.aclose()
|
||||||
|
async with read_stream: # pragma: no branch
|
||||||
|
# First line: \xff\xfe -> U+FFFD U+FFFD -> JSON parse fails -> exception in stream
|
||||||
|
first = await read_stream.receive()
|
||||||
|
assert isinstance(first, Exception)
|
||||||
|
|
||||||
|
# Second line: valid message still comes through
|
||||||
|
second = await read_stream.receive()
|
||||||
|
assert isinstance(second, SessionMessage)
|
||||||
|
assert second.message == JSONRPCMessage(root=valid)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue