fix(client): count clean-EOF reconnects toward retry budget - #3415
Conversation
_handle_reconnection recurses with attempt=0 on the clean-EOF path (stream closed after emitting only a priming event, no JSON-RPC response). The exception path correctly passes attempt+1, but the normal EOF path resets the counter. A no-timeout caller such as subscriptions/listen can therefore reconnect indefinitely instead of resolving with CONNECTION_CLOSED after MAX_RECONNECTION_ATTEMPTS. Pass attempt+1 on both paths so clean EOFs and transport exceptions consume the same budget. Fixes modelcontextprotocol#3307
|
This PR has been closed automatically. This repo only keeps pull requests open when they come from a maintainer, or from a contributor a maintainer has assigned to the linked issue, and you aren't currently assigned to #3307. If a maintainer assigns you to #3307, this PR reopens on its own and there's nothing more you need to do here. Assignment is a maintainer call based on capacity; comments that only ask to be assigned don't factor in. What does help is engaging on the issue itself by confirming the repro, explaining why it matters for your use case, or describing the approach you'd take. You're welcome to keep pushing commits here (just avoid force-pushing, since GitHub can't reopen a rewritten branch), but that on its own won't get the PR reviewed or the issue assigned, and realistically most auto-closed PRs stay closed. There's no need to open a new PR either way. CONTRIBUTING.md has the full reasoning, but in short:
Maintainers: reopen, remove |
Summary
Fixes #3307 — Streamable HTTP clean-EOF reconnections can exceed the request retry budget, causing infinite reconnection loops for long-lived listeners.
Root cause:
_handle_reconnectionrecurses withattempt=0on the clean-EOF path (stream closed after emitting only a priming event, no JSON-RPC response). The exception path correctly passesattempt + 1, but the normal EOF path resets the counter. A no-timeout caller such assubscriptions/listencan therefore reconnect indefinitely instead of resolving withCONNECTION_CLOSEDafterMAX_RECONNECTION_ATTEMPTS.Fix: Pass
attempt + 1on both paths so clean EOFs and transport exceptions consume the same retry budget.Changed files
src/mcp/client/streamable_http.py— passattempt + 1instead of0in the clean-EOF recursion of_handle_reconnection(+1 -1 line)tests/client/test_streamable_http.py— add regression testtest_clean_eof_without_response_counts_toward_reconnection_budgetthat verifies clean-EOF reconnections respectMAX_RECONNECTION_ATTEMPTSTest plan
uv run --frozen pytest tests/client/test_streamable_http.py -q— 29/29 passeduv run --frozen ruff check— all checks passeduv run --frozen pyright— 0 errors