You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
HTTP transport swallows non-2xx status codes causing client to hang #2110
When an MCP server returns non-2xx HTTP status codes (401/403/404/5xx), the Streamable HTTP and SSE transports do not reliably propagate the error to the caller. In the Streamable HTTP transport, post_writer catches exceptions and logs them, but the caller blocks indefinitely waiting for a response on the read stream. HTTP 404 responses are also converted to JSONRPCError(code=32600), losing the original HTTP status information.
Expected Behavior
Non-2xx HTTP responses should be surfaced as exceptions to the caller, not silently logged
The original HTTP status code should be preserved and accessible
Auth-related errors (401/403) should be distinguishable from other failures
Current Behavior
post_writer in Streamable HTTP transport catches and logs errors without forwarding them through the read stream
Callers hang indefinitely waiting for a response
HTTP 404 is converted to JSONRPCError(code=32600), destroying HTTP-level context
I've systematically investigated this issue and confirmed the root cause. Here's what I found:
Root Cause
In both SSE and StreamableHTTP transports, when HTTP errors occur, exceptions are caught and logged but **never sent to **, causing the caller to hang indefinitely waiting for a response that will never arrive.
Specific Issues Found
1. sse.py (lines 147-148):
exceptException:
logger.exception("Error in post_writer")
# Missing: await read_stream_writer.send(exc)
response.raise_for_status() at line 145 raises HTTPStatusError for non-2xx codes
Exception is caught, logged, but never propagated to caller
Result: Caller hangs forever
2. streamable_http.py (lines 481-482):
exceptException:
logger.exception("Error in post_writer")
# Missing: await read_stream_writer.send(exc)
Same issue: exceptions logged but not sent to read_stream_writer
Evidence
Working transports (websocket, stdio) correctly send exceptions:
I have systematically investigated this issue and confirmed the root cause.
Root Cause
In both SSE and StreamableHTTP transports, when HTTP errors occur, exceptions are caught and logged but never sent to the read stream, causing the caller to hang indefinitely waiting for a response that will never arrive.
Specific Issues Found
1. sse.py (lines 147-148):
The post_writer function catches exceptions and only logs them:
response.raise_for_status() at line 145 raises HTTPStatusError for non-2xx codes
Exception is caught, logged, but never propagated to caller
Result: Caller hangs forever
2. streamable_http.py (lines 481-482):
Same issue - exceptions logged but not sent to read_stream_writer
Evidence from Working Transports
Working transports (websocket, stdio) correctly send exceptions to the read stream so the caller can handle them.
Test Confirmation
Created a reproduction test that confirms:
When HTTP errors occur, NO messages are sent to read_stream_writer
Exception is only logged, never propagated
Caller would hang indefinitely
Next Steps
Ready to implement the fix which will:
Catch httpx.HTTPStatusError specifically
Convert to JSONRPCError with HTTP status code preserved
@maxisbey your P1 on error-swallowing in streamable transports is the exact gap that kills unattended MCP workflows — non-2xx errors silently cause indefinite client hangs with zero feedback to the caller. The client thinks it is still connected, so downstream tool calls queue up behind a dead transport.
The pattern we see from operators running multi-MCP setups: one transport failure silently poisons the entire session because nothing detects the hang until a human notices hours later. The fix that worked for one team was a transport-level heartbeat that treats "no response for N seconds" as a hard failure rather than waiting for an error that never comes.
Are you seeing operators hit this in production workflows, or is it still mostly a transport-layer correctness concern?
Purpose: claim the issue per CONTRIBUTING.md ("comment on the issue so we can assign it to you"), explain scope tightly, and acknowledge prior PRs so it's clear we're not duplicating.
Happy to take this on. I'd like to propose a tight, single-purpose PR with the following scope — want to confirm before opening.
Scope (2 parts, both in this issue):
Propagate errors instead of swallowing them. In sse.py::post_writer and streamable_http.py::post_writer, narrow the current except Exception to except httpx.HTTPError as exc and await read_stream_writer.send(exc) — matching the pattern already used in stdio.py and websocket.py. This stops the caller-hangs-indefinitely behavior.
Preserve HTTP status on mapped errors. In streamable_http.py::_handle_post_request, the 404 → INVALID_REQUEST and ≥400 → INTERNAL_ERROR mappings currently discard the HTTP status code. Keep the JSON-RPC code mapping (spec-compliant) but put the HTTP status in ErrorData.data = {"http_status": <code>} and include it in message for INTERNAL_ERROR. Minimal, non-breaking.
Transport-level heartbeat / liveness detection (worth discussing but orthogonal — a PR on top of this fix).
Anything in test_stdio.py or other flaky tests.
About the existing PRs:
fix(client): propagate HTTP transport exceptions to caller #2124 is still open but has merge conflicts and hasn't moved since 2026-03-05. It bundled an unrelated test fix, which may be why it stalled. If you'd prefer I coordinate with @gspeter-max to rebase or carry their commits forward, I'm happy to — otherwise I'll open a fresh, conflict-free PR once this scope is confirmed.
This issue is causing significant problems because the transport swallows the error, our clients hang indefinitely, which effectively "poisons" the session and requires manual restarts. Given that PR #2483 specifically addresses the root cause by propagating httpx errors and preserving status codes, could we get a maintainer review to fast-track this into the next release? This is a critical blocker for unattended operations.
Activity
kumar-pankaj0 commentedon Feb 22, 2026
Investigation Results
I've systematically investigated this issue and confirmed the root cause. Here's what I found:
Root Cause
In both SSE and StreamableHTTP transports, when HTTP errors occur, exceptions are caught and logged but **never sent to **, causing the caller to hang indefinitely waiting for a response that will never arrive.
Specific Issues Found
1.
sse.py(lines 147-148):response.raise_for_status()at line 145 raisesHTTPStatusErrorfor non-2xx codes2.
streamable_http.py(lines 481-482):read_stream_writerEvidence
Working transports (websocket, stdio) correctly send exceptions:
Data Flow Showing the Bug
Test Confirmation
Created reproduction test that confirms:
read_stream_writerNext Steps
Ready to implement the fix which will:
httpx.HTTPStatusErrorspecificallyJSONRPCErrorwith HTTP status code preservedread_stream_writerso caller receives itkumar-pankaj0 commentedon Feb 22, 2026
Investigation Results
I have systematically investigated this issue and confirmed the root cause.
Root Cause
In both SSE and StreamableHTTP transports, when HTTP errors occur, exceptions are caught and logged but never sent to the read stream, causing the caller to hang indefinitely waiting for a response that will never arrive.
Specific Issues Found
1.
sse.py(lines 147-148):The
post_writerfunction catches exceptions and only logs them:response.raise_for_status()at line 145 raisesHTTPStatusErrorfor non-2xx codes2.
streamable_http.py(lines 481-482):Same issue - exceptions logged but not sent to
read_stream_writerEvidence from Working Transports
Working transports (websocket, stdio) correctly send exceptions to the read stream so the caller can handle them.
Test Confirmation
Created a reproduction test that confirms:
read_stream_writerNext Steps
Ready to implement the fix which will:
httpx.HTTPStatusErrorspecificallyJSONRPCErrorwith HTTP status code preservedread_stream_writerso caller receives itfix(client): propagate HTTP transport exceptions to caller
chriscasola commentedon Apr 2, 2026
Will this land pre-v2? It's a pretty significant bug and there is an open PR (or two) with a solution.
fortunexbt commentedon Apr 15, 2026
@maxisbey your P1 on error-swallowing in streamable transports is the exact gap that kills unattended MCP workflows — non-2xx errors silently cause indefinite client hangs with zero feedback to the caller. The client thinks it is still connected, so downstream tool calls queue up behind a dead transport.
The pattern we see from operators running multi-MCP setups: one transport failure silently poisons the entire session because nothing detects the hang until a human notices hours later. The fix that worked for one team was a transport-level heartbeat that treats "no response for N seconds" as a hard failure rather than waiting for an error that never comes.
Are you seeing operators hit this in production workflows, or is it still mostly a transport-layer correctness concern?
fede-kamel commentedon Apr 20, 2026
Draft comment for issue #2110
Purpose: claim the issue per CONTRIBUTING.md ("comment on the issue so we can assign it to you"), explain scope tightly, and acknowledge prior PRs so it's clear we're not duplicating.
Post at: #2110
Happy to take this on. I'd like to propose a tight, single-purpose PR with the following scope — want to confirm before opening.
Scope (2 parts, both in this issue):
Propagate errors instead of swallowing them. In
sse.py::post_writerandstreamable_http.py::post_writer, narrow the currentexcept Exceptiontoexcept httpx.HTTPError as excandawait read_stream_writer.send(exc)— matching the pattern already used instdio.pyandwebsocket.py. This stops the caller-hangs-indefinitely behavior.Preserve HTTP status on mapped errors. In
streamable_http.py::_handle_post_request, the 404 →INVALID_REQUESTand ≥400 →INTERNAL_ERRORmappings currently discard the HTTP status code. Keep the JSON-RPC code mapping (spec-compliant) but put the HTTP status inErrorData.data = {"http_status": <code>}and include it inmessageforINTERNAL_ERROR. Minimal, non-breaking.Explicitly out of scope for this PR:
test_stdio.pyor other flaky tests.About the existing PRs:
Tests I plan to add:
httpx.HTTPStatusErrorwithinanyio.fail_after(3)(no hang).JSONRPCErrorwithdata.http_status == 404.INTERNAL_ERROR.httpx.RequestErrorexception.OK to proceed? If so, happy to get
ready for worklabel and open the PR in the next couple of days.viswaskk commentedon Apr 24, 2026
This issue is causing significant problems because the transport swallows the error, our clients hang indefinitely, which effectively "poisons" the session and requires manual restarts. Given that PR #2483 specifically addresses the root cause by propagating httpx errors and preserving status codes, could we get a maintainer review to fast-track this into the next release? This is a critical blocker for unattended operations.
fede-kamel commentedon Apr 24, 2026
@maxisbey — bumping this, since PR #2483 looks like it addresses the root cause. Any chance of a maintainer review?
fix(client): propagate HTTP transport errors to caller (modelcontextp…
fix(client): propagate SSE POST errors to caller instead of hanging
fix(client): propagate SSE POST errors to caller instead of hanging