Skip to content

fix(server): reject concurrent duplicate JSON-RPC request ids - #3221

Open
arimu1 wants to merge 1 commit into
modelcontextprotocol:mainfrom
arimu1:fix/streamable-http-duplicate-request-id-3137
Open

fix(server): reject concurrent duplicate JSON-RPC request ids#3221
arimu1 wants to merge 1 commit into
modelcontextprotocol:mainfrom
arimu1:fix/streamable-http-duplicate-request-id-3137

Conversation

@arimu1

@arimu1 arimu1 commented Jul 30, 2026

Copy link
Copy Markdown

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_request assigned 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_REQUEST error instead of overwriting. The SSE path reserves the routing slot before EventStore.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 result
  • test_json_response_duplicate_request_id_rejected_while_first_still_in_flight (JSON mode)
  • test_request_id_reuse_after_completion_allowed — sequential reuse still works
  • test_duplicate_request_id_rejected_during_priming_event_store — guard holds across gated priming await
  • uv run --frozen pytest tests/shared/test_streamable_http.py — 69 passed
  • uv run --frozen ruff check / ruff format --check on touched files
  • uv run --frozen pyright on touched files

Disclosure

This change was written with AI assistance (Grok / Cursor); I reviewed the diff, ran the tests above, and can answer questions about the change.

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

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

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.

Concurrent requests sharing a JSON-RPC id on one session receive each other's responses

1 participant