fix(server): reject concurrent duplicate JSON-RPC request ids - #3221
Open
arimu1 wants to merge 1 commit into
Open
fix(server): reject concurrent duplicate JSON-RPC request ids#3221arimu1 wants to merge 1 commit into
arimu1 wants to merge 1 commit into
Conversation
Stateful streamable HTTP keyed per-request streams by bare request id and overwrote in-flight entries, cross-wiring concurrent POSTs that reused an id. Reject the collision with 409 and reserve the slot before the priming await so resumability cannot reopen the race. Fixes modelcontextprotocol#3137
There was a problem hiding this comment.
1 issue found across 2 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/mcp/server/streamable_http.py">
<violation number="1" location="src/mcp/server/streamable_http.py:608">
P2: Distinct valid JSON-RPC ids are being rejected as duplicates because ids are normalized with `str()`: an in-flight numeric `1` blocks a request with string id `"1"` (and `"_GET_stream"` can collide with the internal GET slot). Consider using a type-preserving, non-overlapping routing key consistently for registration and response routing so only the same JSON-RPC id is rejected.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| # responses (one caller receives the other's payload; the other hangs). | ||
| # Reject the collision with 409 rather than silently re-routing. Ids may | ||
| # still be reused after the earlier request has completed. | ||
| if request_id in self._request_streams: |
There was a problem hiding this comment.
P2: Distinct valid JSON-RPC ids are being rejected as duplicates because ids are normalized with str(): an in-flight numeric 1 blocks a request with string id "1" (and "_GET_stream" can collide with the internal GET slot). Consider using a type-preserving, non-overlapping routing key consistently for registration and response routing so only the same JSON-RPC id is rejected.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/mcp/server/streamable_http.py, line 608:
<comment>Distinct valid JSON-RPC ids are being rejected as duplicates because ids are normalized with `str()`: an in-flight numeric `1` blocks a request with string id `"1"` (and `"_GET_stream"` can collide with the internal GET slot). Consider using a type-preserving, non-overlapping routing key consistently for registration and response routing so only the same JSON-RPC id is rejected.</comment>
<file context>
@@ -600,6 +600,19 @@ async def _handle_post_request(self, scope: Scope, request: Request, receive: Re
+ # responses (one caller receives the other's payload; the other hangs).
+ # Reject the collision with 409 rather than silently re-routing. Ids may
+ # still be reused after the earlier request has completed.
+ if request_id in self._request_streams:
+ response = self._create_error_response(
+ f"Conflict: Request id {request_id} is already in flight for this session",
</file context>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stateful streamable HTTP registers each in-flight request's response stream in a per-session dict keyed by the bare JSON-RPC request id.
_handle_post_requestassigned that slot unconditionally, so a second concurrent POST reusing an in-flight id silently overwrote the first request's stream and responses were cross-wired (one caller received the other's payload; the other hung).Both response modes (JSON and SSE) now reject the collision with HTTP 409 Conflict and a JSON-RPC
INVALID_REQUESTerror instead of overwriting. The SSE path reserves the routing slot beforeEventStore.store_event(priming) so a concurrent same-id POST cannot pass the guard during that await — a race left open by a simpler check-before-register approach when resumability is enabled. If priming raises, the reservation is released.Sequential reuse of an id after the earlier request has completed remains allowed.
Fixes #3137
Related
Test plan
test_post_duplicate_request_id_rejected_while_first_still_in_flight(SSE) — second POST gets 409; first still completes with its own resulttest_json_response_duplicate_request_id_rejected_while_first_still_in_flight(JSON mode)test_request_id_reuse_after_completion_allowed— sequential reuse still workstest_duplicate_request_id_rejected_during_priming_event_store— guard holds across gated priming awaituv run --frozen pytest tests/shared/test_streamable_http.py— 69 passeduv run --frozen ruff check/ruff format --checkon touched filesuv run --frozen pyrighton touched filesDisclosure
This change was written with AI assistance (Grok / Cursor); I reviewed the diff, ran the tests above, and can answer questions about the change.