Skip to content

stdio_client with strict decoding withholds fatal read errors until peer stdout EOF #3714

Open

Description

@inanna-malick

Initial Checks

Release line

2.x (current stable)

Description

Explicit encoding_error_handler="strict" decoding of invalid UTF-8 leaves the stdio consumer waiting until the peer closes stdout.

#2454 requests changing default decoding policy. This reproduction explicitly selects strict decoding, so changing the default to replacement would leave this failure-propagation case intact. Termination should become observable without requiring peer exit.

stdout_reader() enters its finally drain before closing the read-stream writer. _drain_stdout() waits for further raw bytes under a cancellation shield. A peer retaining stdout therefore blocks both stream closure and propagation of UnicodeDecodeError.

Example Code

Self-contained failing test reusing the module's existing in-memory FakeProcess and spawn monkeypatch. A byte peer is necessary because a typed server cannot emit malformed UTF-8. Test-only branch; no production changes.

After checking out that branch:

uv run --frozen pytest tests/client/test_stdio.py::test_strict_decode_failure_reaches_consumer_before_peer_stdout_closes -q

Event barriers prove consumption of malformed bytes. After tasks settle, the consumer has not failed. Releasing EOF then surfaces UnicodeDecodeError and completes clean shutdown. The explicit delayed-error assertion fails, without a timeout.

Python & MCP Python SDK

Python 3.14.7, Linux x86_64. Latest stable 2.3.0 (2118f14f8a19bc158d8a1cf90af58d85d187f849) and main 754254c08fe1c3eda18b69da3d336106fe8d72d6: three repetitions on asyncio and Trio all fail identically. The other 35 stdio tests pass against each revision.

AI-assisted investigation and test reproduction, with a human in the loop.

Activity

  1. added
    bugSomething isn't working
    v2Affects the v2 line (2.x on main)
    on Oct 14, 2026
  2. Ben-Home commented on Oct 16, 2026

    @Ben-Home
  3. Vamsi-2581 commented on Oct 16, 2026

    @Vamsi-2581

    I would like to take this. The failing test makes the behavior reproducible, and I plan to make the strict-decode failure observable before peer stdout closes while preserving the existing cleanup behavior. I will add focused regression coverage for the affected async backends and submit a small PR. Could a maintainer assign the issue so I can proceed under the contribution guidelines?

  4. inanna-malick commented on Oct 16, 2026

    @inanna-malick
    Author

    Thanks @Vamsi-2581! I'm happy for you to take the fix forward, subject to maintainer assignment; I'm not reserving it for myself.

    The key behavior is that the consumer can observe the strict-decoding failure while the peer still holds stdout open. Please retain the event-barrier reproduction on asyncio and Trio alongside cleanup coverage, so the fix doesn't merely move the wait elsewhere in shutdown.

    AI-assisted follow-up.

  5. JohnnyWilson16 commented on Nov 6, 2026

    @JohnnyWilson16

    I have a reproducing test and clean fix ready for this.

    The hang occurs because stdout_reader in src/mcp/client/stdio.py awaits _drain_stdout(process) unconditionally in its finally: block before exiting async with read_stream_writer. When strict decoding encounters invalid bytes mid-session, _drain_stdout blocks waiting for peer stdout EOF, delaying read_stream_writer closure and withholding the UnicodeDecodeError from the consumer.

    The fix is a single guard in stdout_reader's finally: block:

    if shutting_down:
        await _drain_stdout(process)

    When a decode error happens mid-session (shutting_down == False), the reader bypasses the blocking drain, exits the context manager, and closes the stream writer immediately so the exception surfaces to the consumer. Normal teardown on exit (shutting_down == True) preserves the complete drain.

    Verified with test_strict_decode_failure_reaches_consumer_before_peer_stdout_closes using FakeProcess across both asyncio and Trio test matrices. All 35 client stdio tests pass cleanly.

    Could a maintainer assign this issue to me so I can submit the PR?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingv2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions