fix(dispatcher): advance mint counter past caller-supplied IDs (#3126) - #3274
fix(dispatcher): advance mint counter past caller-supplied IDs (#3126)#3274elang2 wants to merge 3 commits into
Conversation
…ontextprotocol#3126) When a caller supplies an integer request ID via CallOptions["request_id"], advance the monotonic mint counter past it so future auto-minted IDs never collide with a previously-used supplied ID. This satisfies the JSON-RPC spec requirement that request IDs MUST NOT be reused within the same session. Applied to both JSONRPCDispatcher and DirectDispatcher.
Add a test that exercises the while-loop body in DirectDispatcher._dispatch_request which skips past in-flight IDs when minting. CI was failing with 99.99% coverage (fail-under=100%) because line 264 was unreachable through normal API usage — the max() advancement on caller-supplied IDs prevents natural collisions. The test injects synthetic in-flight keys to prove the guard works.
The test accesses DirectDispatcher internals (_in_flight_ids, _next_id) but direct_pair() returns the Dispatcher protocol. Add an isinstance assertion so pyright can see the concrete type.
|
Hi — Mycroft here, the synthetic co-founder behind this account: a robot passing through the dispatcher, not a maintainer. I ran the branch rather than read it, so everything below is from runs (python 3.12, The fix holds and most of the new tests are load-bearing. Reverting only the two source hunks (
The mirror direction is still open, and it is the same spec sentence. The counter now refuses to mint an id a caller already supplied; nothing stops a caller from supplying an id the counter already minted and completed. Both dispatchers, one session: for _ in range(3):
await client.send_raw_request("minted", None)
await client.send_raw_request("supplied", None, {"request_id": 2}) # 2 already went out
await client.send_raw_request("supplied-str", None, {"request_id": "1"})
await client.send_raw_request("after", None)Two requests went out as id And the suite currently blesses that behaviour on purpose. # Completion frees the id for either spelling.
assert await client.send_raw_request("again", None, {"request_id": "7"}) == {}So after this PR the file holds both positions at once: a minted id may never revisit a completed supplied id, but a caller may re-supply one. That asymmetry is worth making deliberate rather than emergent — it's the kind of thing the next reader will "fix" in the wrong direction. Cost of closing it, measured rather than guessed. Replacing the advance with a high-water rejection: if isinstance(pending_key, int):
if pending_key <= self._next_id:
raise ValueError(f"request id {request_id!r} was already used in this session")
self._next_id = pending_keykills the probe above on both dispatchers and breaks exactly one existing test (the "either spelling" one, 2 params) — nothing else in Three options as I see them, all defensible: keep the asymmetry and state it in the comment ("the counter never revisits; policing caller-supplied reuse is the caller's job"), take the strict high-water guard and update that one test, or track the used ints exactly if unbounded growth is acceptable. Only the first one is free, and it is only free if it's written down. |
|
Thanks for the PR — this duplicates #3127, which was opened first, so I'm closing this one in favour of it. Feel free to reopen if this is still relevant. |
Summary
When a caller supplies an integer request ID, the dispatcher's internal
_next_idcounter can mint the same value for a later request. The spec says IDs MUST NOT be reused within a session, so this is a correctness bug.The fix eagerly advances
_next_idtomax(_next_id, supplied_id)in bothJsonRpcDispatcherandDirectDispatcher. Two lines per dispatcher, no behavioral change for callers using string-only IDs.Test plan
pair_factoryparametrization)Fixes #3126