Skip to content

Commit 9e3d580

Browse files
committed
fix(client): catch httpx transport errors in streamable HTTP post_writer
When an unreachable streamable HTTP server causes httpx.HTTPError inside a request task, complete the pending JSON-RPC waiter with an error response instead of letting the exception crash the outer transport task group. Fixes #915
1 parent 8d0f928 commit 9e3d580

2 files changed

Lines changed: 48 additions & 4 deletions

File tree

src/mcp/client/streamable_http.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -479,10 +479,27 @@ async def _handle_message(session_message: SessionMessage) -> None:
479479
)
480480

481481
async def handle_request_async():
482-
if is_resumption:
483-
await self._handle_resumption_request(ctx)
484-
else:
485-
await self._handle_post_request(ctx)
482+
try:
483+
if is_resumption:
484+
await self._handle_resumption_request(ctx)
485+
else:
486+
await self._handle_post_request(ctx)
487+
except httpx.HTTPError as exc:
488+
# Letting this escape into `tg` would crash the outer task group
489+
# from a different task than the one yielding the streams,
490+
# producing an uncatchable cancel-scope RuntimeError instead of
491+
# a connect error at the caller.
492+
logger.exception("Transport error handling request")
493+
if isinstance(message, JSONRPCRequest):
494+
error_data = ErrorData(
495+
code=INTERNAL_ERROR,
496+
message=f"Transport error: {exc}",
497+
)
498+
error_msg = SessionMessage(
499+
JSONRPCError(jsonrpc="2.0", id=message.id, error=error_data)
500+
)
501+
with contextlib.suppress(anyio.BrokenResourceError, anyio.ClosedResourceError):
502+
await read_stream_writer.send(error_msg)
486503

487504
# If this is a request, start a new task to handle it
488505
if isinstance(message, JSONRPCRequest):
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Regression test for issue #915.
2+
3+
When a streamable HTTP MCP server is unreachable, httpx transport errors must
4+
complete the pending JSON-RPC waiter instead of escaping into the outer task
5+
group (which surfaces as an uncatchable cancel-scope RuntimeError).
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import anyio
11+
import pytest
12+
from mcp_types import INTERNAL_ERROR
13+
14+
from mcp.client.session_group import ClientSessionGroup, StreamableHttpParameters
15+
from mcp.shared.exceptions import MCPError
16+
17+
18+
@pytest.mark.anyio
19+
async def test_unreachable_streamable_http_server_raises_catchable_error() -> None:
20+
async with ClientSessionGroup() as group:
21+
server_params = StreamableHttpParameters(url="http://127.0.0.1:1/mcp/")
22+
with anyio.fail_after(10):
23+
with pytest.raises(MCPError) as exc_info:
24+
await group.connect_to_server(server_params)
25+
26+
assert exc_info.value.code == INTERNAL_ERROR
27+
assert "Transport error" in exc_info.value.message

0 commit comments

Comments
 (0)