Skip to content

Commit 00eb2ff

Browse files
fix(client): re-raise transport exceptions in default message handler
The default ClientSession message_handler checkpointed Exception items from the read stream, so send_request awaiters hung when streamablehttp_client hit sse_read_timeout. Re-raise so _deliver_stream_exception logs the fault at ERROR. Fixes #1401 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d2290ca commit 00eb2ff

2 files changed

Lines changed: 41 additions & 0 deletions

File tree

src/mcp/client/session.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,14 @@ async def __call__(self, message: IncomingMessage) -> None: ... # pragma: no br
250250

251251

252252
async def _default_message_handler(message: IncomingMessage) -> None:
253+
"""Acknowledge server notifications; re-raise transport-level exceptions.
254+
255+
Transport Exception items (e.g. httpx.ReadTimeout from streamablehttp_client
256+
when sse_read_timeout fires) must propagate instead of leaving send_request
257+
awaiters hung. See https://github.com/modelcontextprotocol/python-sdk/issues/1401.
258+
"""
259+
if isinstance(message, Exception):
260+
raise message
253261
await anyio.lowlevel.checkpoint()
254262

255263

tests/client/test_session.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2061,3 +2061,36 @@ def test_intercept_consumes_acks_for_live_routes_and_leaves_malformed_ones():
20612061
# Events deliver but are never consumed - they still tee to message_handler.
20622062
assert intercept("notifications/tools/list_changed", meta) is False
20632063
assert list(route._pending) == [ToolsListChanged()] # pyright: ignore[reportPrivateUsage]
2064+
2065+
2066+
@pytest.mark.anyio
2067+
async def test_default_message_handler_raises_on_transport_exception():
2068+
"""Default handler re-raises transport Exception items instead of swallowing them (#1401)."""
2069+
from mcp.client.session import _default_message_handler
2070+
2071+
boom = RuntimeError("transport went away")
2072+
with pytest.raises(RuntimeError, match="transport went away"):
2073+
await _default_message_handler(boom)
2074+
2075+
await _default_message_handler(types.ToolListChangedNotification())
2076+
2077+
2078+
@pytest.mark.anyio
2079+
async def test_transport_exception_in_stream_logs_at_error_level(caplog):
2080+
"""A transport Exception on the read stream is logged at ERROR via message_handler (#1401)."""
2081+
import logging
2082+
2083+
s2c_send, s2c_recv = anyio.create_memory_object_stream[SessionMessage | Exception](1)
2084+
c2s_send, c2s_recv = anyio.create_memory_object_stream[SessionMessage](1)
2085+
2086+
try:
2087+
with caplog.at_level(logging.ERROR, logger="client"):
2088+
async with ClientSession(s2c_recv, c2s_send):
2089+
await s2c_send.send(RuntimeError("sse_read_timeout fired"))
2090+
await anyio.sleep(0.05)
2091+
await anyio.sleep(0)
2092+
assert any("message_handler raised on transport exception" in rec.message for rec in caplog.records)
2093+
assert "sse_read_timeout fired" in caplog.text
2094+
finally:
2095+
s2c_send.close()
2096+
c2s_recv.close()

0 commit comments

Comments
 (0)