Skip to content

Commit 69a8b7d

Browse files
BabyChrist666claude
andcommitted
fix: handle ClosedResourceError in _handle_message error path (#2064)
When a client disconnects mid-request, the exception handler in _handle_message tries to send_log_message() back to the client. Since the write stream is already closed, this raises ClosedResourceError, which crashes the stateless session with an ExceptionGroup. Wrap the send_log_message call in a try/except that catches ClosedResourceError and BrokenResourceError, since failing to notify a disconnected client is expected and harmless. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2fe56e5 commit 69a8b7d

File tree

2 files changed

+59
-5
lines changed

2 files changed

+59
-5
lines changed

src/mcp/server/lowlevel/server.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -420,11 +420,16 @@ async def _handle_message(
420420
)
421421
case Exception():
422422
logger.error(f"Received exception from stream: {message}")
423-
await session.send_log_message(
424-
level="error",
425-
data="Internal Server Error",
426-
logger="mcp.server.exception_handler",
427-
)
423+
try:
424+
await session.send_log_message(
425+
level="error",
426+
data="Internal Server Error",
427+
logger="mcp.server.exception_handler",
428+
)
429+
except (anyio.ClosedResourceError, anyio.BrokenResourceError):
430+
# Client already disconnected; logging back to
431+
# the client is impossible and harmless to skip.
432+
logger.debug("Could not send error log: client disconnected")
428433
if raise_exceptions:
429434
raise message
430435
case _:

tests/server/test_lowlevel_exception_handling.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from unittest.mock import AsyncMock, Mock
22

3+
import anyio
34
import pytest
45

56
from mcp import types
@@ -72,3 +73,51 @@ async def test_normal_message_handling_not_affected():
7273

7374
# Verify _handle_request was called
7475
server._handle_request.assert_called_once()
76+
77+
78+
@pytest.mark.anyio
79+
@pytest.mark.parametrize(
80+
"error_class",
81+
[anyio.ClosedResourceError, anyio.BrokenResourceError],
82+
)
83+
async def test_exception_handling_with_disconnected_client(error_class: type[Exception]):
84+
"""Test that send_log_message failure due to client disconnect is handled gracefully.
85+
86+
When a client disconnects and the write stream is closed, send_log_message
87+
raises ClosedResourceError or BrokenResourceError. The server should catch
88+
these and not crash the session (fixes #2064).
89+
"""
90+
server = Server("test-server")
91+
session = Mock(spec=ServerSession)
92+
session.send_log_message = AsyncMock(side_effect=error_class())
93+
94+
test_exception = RuntimeError("Client disconnected mid-request")
95+
96+
# Should NOT raise — the ClosedResourceError/BrokenResourceError from
97+
# send_log_message should be caught and suppressed.
98+
await server._handle_message(test_exception, session, {}, raise_exceptions=False)
99+
100+
# send_log_message was still attempted
101+
session.send_log_message.assert_called_once()
102+
103+
104+
@pytest.mark.anyio
105+
@pytest.mark.parametrize(
106+
"error_class",
107+
[anyio.ClosedResourceError, anyio.BrokenResourceError],
108+
)
109+
async def test_exception_handling_with_disconnected_client_raise_exceptions(error_class: type[Exception]):
110+
"""Test that the original exception is still raised when raise_exceptions=True,
111+
even if send_log_message fails due to client disconnect.
112+
"""
113+
server = Server("test-server")
114+
session = Mock(spec=ServerSession)
115+
session.send_log_message = AsyncMock(side_effect=error_class())
116+
117+
test_exception = RuntimeError("Client disconnected mid-request")
118+
119+
# The original exception should still be raised
120+
with pytest.raises(RuntimeError, match="Client disconnected mid-request"):
121+
await server._handle_message(test_exception, session, {}, raise_exceptions=True)
122+
123+
session.send_log_message.assert_called_once()

0 commit comments

Comments
 (0)