Skip to content

fix: catch ClosedResourceError in _handle_message error recovery path#2242

Closed
herakles-dev wants to merge 2 commits into
modelcontextprotocol:mainfrom
herakles-dev:fix/closed-resource-error-in-handle-message
Closed

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

Conversation

@herakles-dev

Copy link
Copy Markdown

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
@BabyChrist666

Copy link
Copy Markdown
Contributor

Hey @herakles-dev — heads up, this is addressed by PR #2072 (opened Feb 16). @maxisbey closed a duplicate issue in its favor earlier. The approach is the same — catching ClosedResourceError in the error recovery path.

@herakles-dev

Copy link
Copy Markdown
Author

Thanks for pointing this out @BabyChrist666 — completely my oversight, I should have caught #2072 in my search. Your fix predates this by weeks and covers the same path. Closing in favor of yours. Appreciate the heads-up!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants