Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions src/mcp/client/streamable_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,10 +479,27 @@ async def _handle_message(session_message: SessionMessage) -> None:
)

async def handle_request_async():
if is_resumption:
await self._handle_resumption_request(ctx)
else:
await self._handle_post_request(ctx)
try:
if is_resumption:
await self._handle_resumption_request(ctx)
else:
await self._handle_post_request(ctx)
except httpx.HTTPError as exc:
# Letting this escape into `tg` would crash the outer task group
# from a different task than the one yielding the streams,
# producing an uncatchable cancel-scope RuntimeError instead of
# a connect error at the caller.
logger.exception("Transport error handling request")
if isinstance(message, JSONRPCRequest):
error_data = ErrorData(
code=INTERNAL_ERROR,
message=f"Transport error: {exc}",
)
error_msg = SessionMessage(
JSONRPCError(jsonrpc="2.0", id=message.id, error=error_data)
)
with contextlib.suppress(anyio.BrokenResourceError, anyio.ClosedResourceError):
await read_stream_writer.send(error_msg)

# If this is a request, start a new task to handle it
if isinstance(message, JSONRPCRequest):
Expand Down
27 changes: 27 additions & 0 deletions tests/issues/test_915_streamable_http_unreachable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Regression test for issue #915.

When a streamable HTTP MCP server is unreachable, httpx transport errors must
complete the pending JSON-RPC waiter instead of escaping into the outer task
group (which surfaces as an uncatchable cancel-scope RuntimeError).
"""

from __future__ import annotations

import anyio
import pytest
from mcp_types import INTERNAL_ERROR

from mcp.client.session_group import ClientSessionGroup, StreamableHttpParameters
from mcp.shared.exceptions import MCPError


@pytest.mark.anyio
async def test_unreachable_streamable_http_server_raises_catchable_error() -> None:
async with ClientSessionGroup() as group:
server_params = StreamableHttpParameters(url="http://127.0.0.1:1/mcp/")
with anyio.fail_after(10):
with pytest.raises(MCPError) as exc_info:
await group.connect_to_server(server_params)

assert exc_info.value.code == INTERNAL_ERROR
assert "Transport error" in exc_info.value.message
Loading