Skip to content

fix: prevent infinite reconnection loop when SSE stream drops without response#2512

Open
dragogargo wants to merge 2 commits intomodelcontextprotocol:mainfrom
dragogargo:fix/infinite-reconnection-loop-2393
Open

fix: prevent infinite reconnection loop when SSE stream drops without response#2512
dragogargo wants to merge 2 commits intomodelcontextprotocol:mainfrom
dragogargo:fix/infinite-reconnection-loop-2393

Conversation

@dragogargo
Copy link
Copy Markdown

Motivation and Context

Fixes #2393. _handle_reconnection() resets the attempt counter to 0 when a reconnection succeeds at the HTTP level but the stream ends without delivering a complete JSON-RPC response. This makes MAX_RECONNECTION_ATTEMPTS ineffective — the counter only applies to consecutive exceptions, not total reconnection attempts. If the server accepts the connection but the stream drops repeatedly (sending only priming events), the client retries forever.

In production this causes MCP client coroutines to hang indefinitely when a server experiences transient stream drops.

How Has This Been Tested?

  • Regression test (test_handle_reconnection_stops_after_max_attempts) that mocks aconnect_sse to simulate a server sending only priming events then dropping. Verifies the client gives up after MAX_RECONNECTION_ATTEMPTS and sends a JSONRPCError back to the caller.
  • All 62 existing test_streamable_http.py tests pass, including test_streamable_http_multiple_reconnections (3 legitimate reconnections with real data).

Changes

  1. Smart attempt counter: track whether real SSE data (not just priming events) was received during reconnection. Reset counter only when progress was made; increment when server only sends empty priming events.
  2. Error reporting: when max attempts are exceeded, send a JSONRPCError back through read_stream_writer so call_tool returns an error instead of hanging forever.
  3. Regression test: in-memory unit test with mocked SSE to verify the fix.

Breaking Changes

No.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Andrey Barchenkov added 2 commits April 27, 2026 19:17
… response

When the server accepts SSE connections but closes the stream without
delivering a complete JSON-RPC response, the client retried forever
because _handle_reconnection reset the attempt counter to 0 on each
reconnection.

Now the attempt counter only resets when real data (not just priming
events) was received, indicating the server made progress. When the
server only sends empty priming events and drops, the counter increments
and the client gives up after MAX_RECONNECTION_ATTEMPTS.

Also report a JSONRPCError back to the caller when max attempts are
exceeded, so call_tool returns an error instead of hanging forever.

Fixes modelcontextprotocol#2393
The isinstance check for JSONRPCRequest always evaluates to True
because _handle_reconnection is only called for request messages.
Mark the branch with pragma: no branch to satisfy 100% branch coverage.
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.

StreamableHTTP client: _handle_reconnection resets attempt counter to 0, causing infinite retry loop

1 participant