Skip to content

fix: catch ClosedResourceError in _handle_message error recovery path#2242

Open
herakles-dev wants to merge 2 commits intomodelcontextprotocol:mainfrom
herakles-dev:fix/closed-resource-error-in-handle-message
Open

fix: catch ClosedResourceError in _handle_message error recovery path#2242
herakles-dev wants to merge 2 commits intomodelcontextprotocol:mainfrom
herakles-dev:fix/closed-resource-error-in-handle-message

Conversation

@herakles-dev
Copy link

Summary

When a client disconnects while a stateless streamable-HTTP server is reading the request body, _handle_message catches the stream exception but then tries to send_log_message() back to the already-disconnected client. Since the write stream is closed, this raises ClosedResourceError, which propagates 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.

Fix

Wrap the send_log_message() call in _handle_message with a try/except for anyio.ClosedResourceError and anyio.BrokenResourceError. This matches the existing pattern already used throughout streamable_http.py (lines 589, 915, 1011, 1020). Failing to notify a disconnected client is expected and harmless.

Changes

  • src/mcp/server/lowlevel/server.py — catch ClosedResourceError/BrokenResourceError around send_log_message() in the Exception branch of _handle_message, log at debug level instead of crashing
  • tests/server/test_lowlevel_exception_handling.py — two regression tests:
    • test_exception_handling_tolerates_closed_write_stream — parametrized over both error types, verifies no crash when send_log_message raises
    • test_exception_handling_closed_stream_still_reraises_when_requested — verifies raise_exceptions=True still propagates the original error even when the write stream is closed

Test plan

  • New regression tests cover both ClosedResourceError and BrokenResourceError scenarios
  • Existing exception handling tests continue to pass (no behavioral change for connected clients)
  • raise_exceptions=True still correctly re-raises the original error after the log attempt fails

Fixes #2064

When a client disconnects while a stateless streamable-HTTP server is
reading the request body, 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 unhandled ExceptionGroup.

Wrap the send_log_message() call in a try/except for ClosedResourceError
and BrokenResourceError, matching the pattern already used throughout
streamable_http.py (lines 589, 915, 1011, 1020). Failing to notify a
disconnected client is expected and harmless.

Github-Issue: modelcontextprotocol#2064
Reported-by: dannygoldstein
Single-line the debug message as ruff format prefers.

Github-Issue: modelcontextprotocol#2064
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ClientDisconnect during _handle_post_request crashes stateless session with ClosedResourceError

1 participant