Skip to content

Commit 6d06e16

Browse files
g97iulio1609Copilot
andcommitted
fix: catch ClosedResourceError in _handle_message error recovery path
When a client disconnects during a request, _handle_message receives the exception and tries to send_log_message() back to notify the client. Since the write stream is already closed, this raises ClosedResourceError (or BrokenResourceError), which is unhandled in the TaskGroup and crashes the stateless session with an ExceptionGroup. Wrap the send_log_message() call in _handle_message's Exception branch with a try/except for ClosedResourceError and BrokenResourceError, since failing to notify a disconnected client is expected and harmless. Fixes #2064 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 62575ed commit 6d06e16

File tree

2 files changed

+68
-5
lines changed

2 files changed

+68
-5
lines changed

src/mcp/server/lowlevel/server.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -417,11 +417,16 @@ 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(
428+
"Could not send error log to client (stream already closed)"
429+
)
425430
if raise_exceptions:
426431
raise message
427432
case _:

tests/server/test_lowlevel_exception_handling.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,61 @@ 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+
async def test_exception_handling_survives_closed_write_stream():
79+
"""When the client disconnects, send_log_message may raise ClosedResourceError.
80+
81+
The server should catch it gracefully instead of crashing the session.
82+
Regression test for GH-2064.
83+
"""
84+
import anyio
85+
86+
server = Server("test-server")
87+
session = Mock(spec=ServerSession)
88+
session.send_log_message = AsyncMock(side_effect=anyio.ClosedResourceError)
89+
90+
test_exception = RuntimeError("client disconnected")
91+
92+
# Should NOT raise — the ClosedResourceError from send_log_message is caught
93+
await server._handle_message(test_exception, session, {}, raise_exceptions=False)
94+
95+
session.send_log_message.assert_called_once()
96+
97+
98+
@pytest.mark.anyio
99+
async def test_exception_handling_survives_broken_write_stream():
100+
"""When the write stream is broken, send_log_message may raise BrokenResourceError.
101+
102+
The server should catch it gracefully.
103+
Regression test for GH-2064.
104+
"""
105+
import anyio
106+
107+
server = Server("test-server")
108+
session = Mock(spec=ServerSession)
109+
session.send_log_message = AsyncMock(side_effect=anyio.BrokenResourceError)
110+
111+
test_exception = RuntimeError("broken pipe")
112+
113+
await server._handle_message(test_exception, session, {}, raise_exceptions=False)
114+
115+
session.send_log_message.assert_called_once()
116+
117+
118+
@pytest.mark.anyio
119+
async def test_exception_handling_raises_when_configured_despite_closed_stream():
120+
"""With raise_exceptions=True, the original error is still re-raised
121+
even when send_log_message fails with ClosedResourceError.
122+
"""
123+
import anyio
124+
125+
server = Server("test-server")
126+
session = Mock(spec=ServerSession)
127+
session.send_log_message = AsyncMock(side_effect=anyio.ClosedResourceError)
128+
129+
test_exception = RuntimeError("original error")
130+
131+
with pytest.raises(RuntimeError, match="original error"):
132+
await server._handle_message(test_exception, session, {}, raise_exceptions=True)

0 commit comments

Comments
 (0)