Visitar URL original
HTTP transport swallows non-2xx status codes causing client to hang · Issue #2110 · modelcontextprotocol/python-sdk · GitHub
Skip to content

HTTP transport swallows non-2xx status codes causing client to hang #2110

Description

@maxisbey

Summary

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

Affected Code

  • src/mcp/client/streamable_http.py (post_writer)
  • src/mcp/client/sse.py (sse_reader)

Related

Activity

  1. added
    bugSomething isn't working
    v2Affects the v2 line (2.x on main)
    P1Significant bug affecting many users, highly requested feature
    on Feb 19, 2026
  2. kumar-pankaj0 commented on Feb 22, 2026

    @kumar-pankaj0

    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):

    except Exception:
        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):

    except Exception:
        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:

    # websocket.py (working ✅)
    except ValidationError as exc:
        await read_stream_writer.send(exc)
    
    # stdio.py (working ✅)  
    except Exception as exc:
        logger.exception("...")
        await read_stream_writer.send(exc)  # ← Sends exception!

    Data Flow Showing the Bug

    Caller: await session.send_request()
        ↓
    post_writer: response = await client.post(...)
        ↓
    Server returns 401
        ↓
    httpx raises HTTPStatusError
        ↓
    post_writer catches exception
        ↓
    logger.exception("Error") ← Only logs!
        ↓
    Function exits
        ↓
    Caller: await response_stream.receive() ← Hangs forever ⏳
    

    Test Confirmation

    Created reproduction test that confirms:

    • When 401 error occurs, NO messages are sent to read_stream_writer
    • Exception is only logged, never propagated
    • Caller hangs indefinitely

    Next Steps

    Ready to implement the fix which will:

    1. Catch httpx.HTTPStatusError specifically
    2. Convert to JSONRPCError with HTTP status code preserved
    3. Send to read_stream_writer so caller receives it
  3. kumar-pankaj0 commented on Feb 22, 2026

    @kumar-pankaj0

    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_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:

    1. Catch httpx.HTTPStatusError specifically
    2. Convert to JSONRPCError with HTTP status code preserved
    3. Send to read_stream_writer so caller receives it
  4. chriscasola commented on Apr 2, 2026

    @chriscasola

    Will this land pre-v2? It's a pretty significant bug and there is an open PR (or two) with a solution.

  5. fortunexbt commented on Apr 15, 2026

    @fortunexbt

    @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?

  6. fede-kamel commented on Apr 20, 2026

    @fede-kamel

    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):

    1. 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.

    2. 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.

    Explicitly out of scope for this PR:

    • Adding a full typed error hierarchy (that's Introduce typed error classes with metadata #1742 and should stay separate).
    • 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:

    Tests I plan to add:

    • 401/403 on POST → caller receives httpx.HTTPStatusError within anyio.fail_after(3) (no hang).
    • 404 on POST (with JSONRPCRequest) → caller receives JSONRPCError with data.http_status == 404.
    • 500 on POST → same shape for INTERNAL_ERROR.
    • Network error on POST (simulated via mock transport) → caller receives the httpx.RequestError exception.

    OK to proceed? If so, happy to get ready for work label and open the PR in the next couple of days.

  7. viswaskk commented on Apr 24, 2026

    @viswaskk

    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.

  8. fede-kamel commented on Apr 24, 2026

    @fede-kamel

    @maxisbey — bumping this, since PR #2483 looks like it addresses the root cause. Any chance of a maintainer review?

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

    P1Significant bug affecting many users, highly requested featurebugSomething 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