Skip to content

fix: propagate HTTP errors from transports instead of silently logging#2249

Open
MaxwellCalkin wants to merge 4 commits intomodelcontextprotocol:mainfrom
MaxwellCalkin:fix/http-error-propagation
Open

fix: propagate HTTP errors from transports instead of silently logging#2249
MaxwellCalkin wants to merge 4 commits intomodelcontextprotocol:mainfrom
MaxwellCalkin:fix/http-error-propagation

Conversation

@MaxwellCalkin
Copy link

Note: This PR was authored by Claude (AI), operated by @MaxwellCalkin.

Summary

Fixes #2110

When an MCP server returns non-2xx HTTP status codes (401/403/404/5xx), the Streamable HTTP and SSE transports silently log the error but don't propagate it to the caller. The caller hangs indefinitely waiting for a response on the read stream.

Changes

New HttpError exception class (src/mcp/shared/exceptions.py):

  • Preserves the original HTTP status code
  • is_auth_error property to distinguish 401/403 from other failures
  • Optional body field for response body context
  • Exported from the top-level mcp package

Streamable HTTP transport (src/mcp/client/streamable_http.py):

  • _handle_post_request: 401/403 responses now raise HttpError (in addition to sending a JSONRPCError for request messages), so the caller gets an exception instead of hanging
  • _handle_post_request: Generic 4xx/5xx responses also raise HttpError instead of silently returning
  • _handle_post_request: HTTP 404 error message now includes "(HTTP 404)" and data={"http_status": 404} to preserve HTTP-level context
  • _handle_post_request: All error responses include data={"http_status": <code>} in the JSONRPCError
  • post_writer: handle_request_async now wraps calls in try/except and forwards exceptions through read_stream_writer.send(exc) so callers don't hang
  • post_writer: Outer exception handler also forwards errors through the read stream

SSE transport (src/mcp/client/sse.py):

  • post_writer: 401/403 responses detected before raise_for_status() and forwarded as HttpError through the read stream
  • post_writer: httpx.HTTPStatusError from raise_for_status() converted to HttpError and forwarded through the read stream
  • post_writer: Generic exceptions forwarded through the read stream instead of being silently logged

Before

# Caller hangs forever waiting for a response that will never come
async with streamable_http_client("https://example.com/mcp") as (read, write):
    await write.send(SessionMessage(request))
    response = await read.receive()  # blocks indefinitely on 401/403/5xx

After

# Caller receives an HttpError exception through the read stream
async with streamable_http_client("https://example.com/mcp") as (read, write):
    await write.send(SessionMessage(request))
    response = await read.receive()  # receives HttpError or JSONRPCError with http_status

# Or catch it directly
from mcp.shared.exceptions import HttpError
try:
    ...
except HttpError as e:
    if e.is_auth_error:
        print(f"Auth failed: {e.status_code}")

Test plan

  • Verify existing tests pass (no behavioral change for 2xx responses)
  • Verify 401/403 responses raise HttpError with is_auth_error == True
  • Verify 404 responses include HTTP status in error data
  • Verify 5xx responses raise HttpError and don't cause caller to hang
  • Verify HttpError is importable from mcp top-level package

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant