From b6139995323cdeaffe90e627de8daa4101f93ea3 Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sat, 28 Feb 2026 12:10:06 +0100 Subject: [PATCH 1/2] fix: handle ClosedResourceError when logging errors to disconnected clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a client disconnects mid-request, _handle_message catches the resulting Exception and tries to send_log_message() back to the client. Since the session 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 PR #1384, which fixed the message router loop. This bug is in the error recovery path: catch exception → try to log it to client → write stream closed → crash. Fix: catch ClosedResourceError and BrokenResourceError around the send_log_message call in _handle_message, since failing to notify a disconnected client is expected and harmless. Fixes #2064 --- src/mcp/server/lowlevel/server.py | 15 ++++-- .../test_lowlevel_exception_handling.py | 46 +++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index aee644040..2861eabb1 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -417,11 +417,16 @@ async def _handle_message( ) case Exception(): logger.error(f"Received exception from stream: {message}") - await session.send_log_message( - level="error", - data="Internal Server Error", - logger="mcp.server.exception_handler", - ) + try: + await session.send_log_message( + level="error", + data="Internal Server Error", + logger="mcp.server.exception_handler", + ) + except (anyio.ClosedResourceError, anyio.BrokenResourceError): + logger.warning( + "Could not send error log to client: write stream already closed" + ) if raise_exceptions: raise message case _: diff --git a/tests/server/test_lowlevel_exception_handling.py b/tests/server/test_lowlevel_exception_handling.py index 848b35b29..3d0955354 100644 --- a/tests/server/test_lowlevel_exception_handling.py +++ b/tests/server/test_lowlevel_exception_handling.py @@ -72,3 +72,49 @@ async def test_normal_message_handling_not_affected(): # Verify _handle_request was called server._handle_request.assert_called_once() + + +@pytest.mark.anyio +async def test_exception_handling_tolerates_closed_write_stream(): + """send_log_message should not crash when the client has already disconnected.""" + import anyio + + server = Server("test-server") + session = Mock(spec=ServerSession) + session.send_log_message = AsyncMock(side_effect=anyio.ClosedResourceError) + + test_exception = RuntimeError("client went away") + + # Must not raise — the ClosedResourceError should be caught internally + await server._handle_message(test_exception, session, {}, raise_exceptions=False) + + +@pytest.mark.anyio +async def test_exception_handling_tolerates_broken_write_stream(): + """send_log_message should not crash when the write stream is broken.""" + import anyio + + server = Server("test-server") + session = Mock(spec=ServerSession) + session.send_log_message = AsyncMock(side_effect=anyio.BrokenResourceError) + + test_exception = RuntimeError("client went away") + + # Must not raise — the BrokenResourceError should be caught internally + await server._handle_message(test_exception, session, {}, raise_exceptions=False) + + +@pytest.mark.anyio +async def test_exception_handling_closed_stream_with_raise_exceptions(): + """Even with raise_exceptions=True, ClosedResourceError from log should be tolerated.""" + import anyio + + server = Server("test-server") + session = Mock(spec=ServerSession) + session.send_log_message = AsyncMock(side_effect=anyio.ClosedResourceError) + + test_exception = RuntimeError("client went away") + + # raise_exceptions re-raises the *original* exception, not the ClosedResourceError + with pytest.raises(RuntimeError, match="client went away"): + await server._handle_message(test_exception, session, {}, raise_exceptions=True) From ab6a25be10b988e235d2631521a8a337da7d85f2 Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sat, 28 Feb 2026 14:05:25 +0100 Subject: [PATCH 2/2] style: apply ruff format to server.py --- src/mcp/server/lowlevel/server.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index 2861eabb1..21cbccc8e 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -424,9 +424,7 @@ async def _handle_message( logger="mcp.server.exception_handler", ) except (anyio.ClosedResourceError, anyio.BrokenResourceError): - logger.warning( - "Could not send error log to client: write stream already closed" - ) + logger.warning("Could not send error log to client: write stream already closed") if raise_exceptions: raise message case _: