Conversation
A POST whose HTTP exchange itself fails (server drops the connection before sending a response, connect/read errors, timeouts) escaped _handle_post_request unhandled. The exception propagated out of the per-request task, cancelling the transport's task group: the write stream closed, so every subsequent request on the session failed with 'Connection closed' and transport teardown surfaced the raw error again as an unhandled ExceptionGroup. Resolve only the in-flight request with a synthesized CONNECTION_CLOSED error via the same _resolve_abandoned_request path already used for 202-accepted and non-resumable SSE-drop outcomes, so the session and its transport survive and later requests go out on a fresh connection, matching the TypeScript client's recovery behavior. Fixes modelcontextprotocol#3522
|
This PR has been closed automatically. This repo only keeps pull requests open when they come from a maintainer, or from a contributor a maintainer has assigned to the linked issue, and you aren't currently assigned to #3522. If a maintainer assigns you to #3522, this PR reopens on its own and there's nothing more you need to do here. Assignment is a maintainer call based on capacity; comments that only ask to be assigned don't factor in. What does help is engaging on the issue itself by confirming the repro, explaining why it matters for your use case, or describing the approach you'd take. You're welcome to keep pushing commits here (just avoid force-pushing, since GitHub can't reopen a rewritten branch), but that on its own won't get the PR reviewed or the issue assigned, and realistically most auto-closed PRs stay closed. There's no need to open a new PR either way. CONTRIBUTING.md has the full reasoning, but in short:
Maintainers: reopen, remove |
Fixes #3522
What was wrong
A request POST whose HTTP exchange itself failed — server drops the connection before sending a response, connect/read errors, timeouts — escaped
_handle_post_requestunhandled. The exception propagated out of the per-request task (_run_request_posthas no guard), cancelling the transport's task group:MCPError: Connection closed;streamable_http_client(...)re-raised the raw error as an unhandledExceptionGroup.The TypeScript client recovers from the same server behavior (request fails, next request succeeds), and the Python SDK already contains the sibling failure modes — 202 Accepted, non-resumable SSE drops, exhausted reconnection attempts — by resolving just the in-flight request. This was the one POST failure path that bypassed that pattern.
The change
Catch
httpx2.TransportErroraround the POST exchange in_handle_post_requestand resolve the in-flight request with a synthesizedCONNECTION_CLOSEDerror through the existing_resolve_abandoned_requesthelper (which already tolerates a tearing-down read stream). The session and its transport survive; the next request goes out on a fresh connection. Notifications with a transport error are logged, not fatal to the task group.Verification
mainagainstmcp-failure-lab@0.9.0(HTTP transport): interrupted call →Connection closedon the nextping→ExceptionGroupon cleanup. With the fix: the interrupted call fails withMCPError: HTTP transport error: …, the followingpingsucceeds on the same client, cleanup is clean.test_a_post_transport_error_fails_only_that_request_and_keeps_the_session(MockTransport-based, following the existing pattern); verified it fails on unmodifiedmainand passes with the fix.pytest tests/client/— 780 passed, 1 skipped, 1 xfailed (no regressions).ruff check/ruff formatclean;pyrightclean on both touched files.