Skip to content

Commit 74b476f

Browse files
g97iulio1609Copilot
andcommitted
fix: catch ClosedResourceError when sending error log to disconnected client
When a client disconnects during request handling, _handle_message's error handler tries to send_log_message() back to the client. Since the write stream is already closed, this raises ClosedResourceError (or BrokenResourceError), which is unhandled and crashes the stateless session with an ExceptionGroup. This is a different code path from what PR #1384 fixed (message router loop). This bug is in the error recovery path: catch exception → try to log it to client → write stream already closed → crash. Added try/except around send_log_message() to catch both ClosedResourceError and BrokenResourceError, logging a debug message instead of crashing. Fixes #2064 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 62575ed commit 74b476f

File tree

2 files changed

+57
-5
lines changed

2 files changed

+57
-5
lines changed

src/mcp/server/lowlevel/server.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -417,11 +417,14 @@ async def _handle_message(
417417
)
418418
case Exception():
419419
logger.error(f"Received exception from stream: {message}")
420-
await session.send_log_message(
421-
level="error",
422-
data="Internal Server Error",
423-
logger="mcp.server.exception_handler",
424-
)
420+
try:
421+
await session.send_log_message(
422+
level="error",
423+
data="Internal Server Error",
424+
logger="mcp.server.exception_handler",
425+
)
426+
except (anyio.ClosedResourceError, anyio.BrokenResourceError):
427+
logger.debug("Could not send error log to client: connection already closed")
425428
if raise_exceptions:
426429
raise message
427430
case _:

tests/server/test_lowlevel_exception_handling.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,52 @@ async def test_normal_message_handling_not_affected():
7272

7373
# Verify _handle_request was called
7474
server._handle_request.assert_called_once()
75+
76+
77+
@pytest.mark.anyio
78+
@pytest.mark.parametrize(
79+
"error_class",
80+
[
81+
pytest.param("ClosedResourceError", id="closed"),
82+
pytest.param("BrokenResourceError", id="broken"),
83+
],
84+
)
85+
async def test_exception_handling_tolerates_closed_write_stream(error_class: str):
86+
"""send_log_message failure on a disconnected client should not crash the server.
87+
88+
Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/2064.
89+
When a client disconnects during request handling, the error handler tries to
90+
send a log message back. If the write stream is already closed, the resulting
91+
ClosedResourceError/BrokenResourceError should be caught silently.
92+
"""
93+
import anyio
94+
95+
server = Server("test-server")
96+
session = Mock(spec=ServerSession)
97+
98+
exc_cls = getattr(anyio, error_class)
99+
session.send_log_message = AsyncMock(side_effect=exc_cls())
100+
101+
test_exception = RuntimeError("Client disconnected")
102+
103+
# Should NOT raise — the ClosedResourceError from send_log_message is caught
104+
await server._handle_message(test_exception, session, {}, raise_exceptions=False)
105+
106+
# send_log_message was still attempted
107+
session.send_log_message.assert_called_once()
108+
109+
110+
@pytest.mark.anyio
111+
async def test_exception_handling_reraises_with_closed_stream():
112+
"""When raise_exceptions=True AND the write stream is closed, the original
113+
exception should still be re-raised (not the ClosedResourceError)."""
114+
import anyio
115+
116+
server = Server("test-server")
117+
session = Mock(spec=ServerSession)
118+
session.send_log_message = AsyncMock(side_effect=anyio.ClosedResourceError())
119+
120+
test_exception = RuntimeError("Original error")
121+
122+
with pytest.raises(RuntimeError, match="Original error"):
123+
await server._handle_message(test_exception, session, {}, raise_exceptions=True)

0 commit comments

Comments
 (0)