Skip to content

Streamable HTTP clean EOF reconnects can exceed the request retry budget #3307

Description

@jstar0

Description

The streamable HTTP client can exceed its per-request SSE reconnection budget when each reconnect opens successfully, emits only an id-bearing priming event, and then reaches EOF without a JSON-RPC response.

In that case _handle_reconnection() currently recurses with attempt=0 after the clean EOF path. The exception path increments the counter, but the normal EOF-without-response path resets it, so a no-timeout request such as subscriptions/listen can keep reconnecting instead of resolving the waiter with CONNECTION_CLOSED after MAX_RECONNECTION_ATTEMPTS.

Reproduction

Drive StreamableHTTPTransport._handle_reconnection() with a mock HTTP transport that returns these per-request SSE responses:

  1. Reconnect 1: id: evt-1 with empty data, then EOF.
  2. Reconnect 2: id: evt-2 with empty data, then EOF.
  3. Reconnect 3: a JSON-RPC success response.

Starting from Last-Event-ID: evt-0 and retry_interval_ms=0, current main makes the third HTTP request and delivers the success response. I expected the client to stop after the two configured reconnect attempts, emit a JSONRPCError for the original request with CONNECTION_CLOSED, and only send Last-Event-ID: evt-0 and Last-Event-ID: evt-1.

Expected Behavior

Each per-request reconnect that reaches EOF without delivering a JSON-RPC response should consume the reconnect budget. That keeps request-scoped SSE drops consistent whether they end by transport exception or by clean EOF, and prevents no-timeout callers from staying parked forever when a server repeatedly closes resumable streams without producing the response.

Local Verification

I have a local regression test that fails on current main because the third reconnect response is accepted, then passes when the clean EOF path recurses with attempt + 1.

Commands run locally:

  • uv run --frozen pytest tests/client/test_streamable_http.py::test_empty_resumable_sse_reconnects_count_toward_the_request_budget -q
  • uv run --frozen pytest tests/client/test_streamable_http.py -q
  • uv run --frozen ruff check src/mcp/client/streamable_http.py tests/client/test_streamable_http.py
  • uv run --frozen ruff format --check src/mcp/client/streamable_http.py tests/client/test_streamable_http.py
  • uv run --frozen pyright src/mcp/client/streamable_http.py tests/client/test_streamable_http.py
  • UV_FROZEN=1 uv run --frozen strict-no-cover

Disclosure: I used AI assistance to help prepare this report and a local patch; I reviewed the reproduction, root cause, and test results.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Moderate issues affecting some users, edge cases, potentially valuable featurebugSomething isn't workingneeds confirmationNeeds confirmation that the PR is actually required or needed.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions