Skip to content

Commit f615b44

Browse files
Christian-Sidakclaude
andcommitted
fix: default encoding_error_handler to 'replace' in StdioServerParameters
Match the server-side behavior from PR #2302: replace invalid UTF-8 bytes with U+FFFD so malformed child output surfaces as a JSON parse error in the read stream instead of crashing the transport task group. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d5b9155 commit f615b44

2 files changed

Lines changed: 39 additions & 1 deletion

File tree

src/mcp/client/stdio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ class StdioServerParameters(BaseModel):
9292
Defaults to utf-8.
9393
"""
9494

95-
encoding_error_handler: Literal["strict", "ignore", "replace"] = "strict"
95+
encoding_error_handler: Literal["strict", "ignore", "replace"] = "replace"
9696
"""
9797
The text encoding error handler.
9898

tests/client/test_stdio.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,44 @@ async def test_stdio_client_graceful_stdin_exit():
501501
)
502502

503503

504+
@pytest.mark.anyio
505+
async def test_stdio_client_invalid_utf8():
506+
"""Malformed UTF-8 from the child must not crash the transport.
507+
508+
Invalid bytes are replaced with U+FFFD (default encoding_error_handler),
509+
which fails JSON parsing and arrives as an in-stream exception. The
510+
following valid JSON-RPC line must still be delivered as a SessionMessage.
511+
"""
512+
valid_line = '{"jsonrpc":"2.0","id":1,"method":"ping"}'
513+
script = textwrap.dedent(
514+
f"""
515+
import sys, time
516+
sys.stdout.buffer.write(b"\\xff\\xfe\\n")
517+
sys.stdout.buffer.write({valid_line!r}.encode() + b"\\n")
518+
sys.stdout.buffer.flush()
519+
time.sleep(1)
520+
"""
521+
)
522+
523+
server_params = StdioServerParameters(
524+
command=sys.executable,
525+
args=["-c", script],
526+
)
527+
528+
items: list[SessionMessage | Exception] = []
529+
530+
with anyio.fail_after(5.0):
531+
async with stdio_client(server_params) as (read_stream, write_stream):
532+
async for item in read_stream:
533+
items.append(item)
534+
if len(items) >= 2:
535+
break
536+
537+
assert len(items) == 2
538+
assert isinstance(items[0], Exception)
539+
assert isinstance(items[1], SessionMessage)
540+
541+
504542
@pytest.mark.anyio
505543
async def test_stdio_client_stdin_close_ignored():
506544
"""Test that when a process ignores stdin closure, the shutdown sequence

0 commit comments

Comments
 (0)