Skip to content

Echo the request id in session-not-found 404s - #6032

Closed
jhrozek wants to merge 1 commit into
mainfrom
fix-session-notfound-id-echo
Closed

Echo the request id in session-not-found 404s#6032
jhrozek wants to merge 1 commit into
mainfrom
fix-session-notfound-id-echo

Conversation

@jhrozek

@jhrozek jhrozek commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • The transparent proxy's unknown-session guard returned a -32001 (session not found) 404 whose body hardcoded "id":null. Every sibling rejection path — mcp.ClassificationErrorResponse, and the streamable proxy's resolveSessionForRequest — echoes the incoming JSON-RPC id, so a client correlating responses by id got null for this one error class and nothing else.
  • session.NotFoundResponse now takes a requestID and forwards it to NotFoundBody, which already accepted one. The transparent proxy passes the id it has already parsed.
  • The parsed id was block-scoped inside the classification block in RoundTrip and not visible at the guard 20 lines below. Hoisting it to rpcID is the whole production change.

Closes #5945

Type of change

  • Bug fix
  • New feature
  • Refactoring (no behavior change)
  • Dependency update
  • Documentation
  • Other (describe):

Test plan

  • Unit tests — scoped, see note
  • E2E tests (task test-e2e)
  • Linting (task lint-fix)
  • Manual testing

Note on scope, so the boxes aren't read as more than they are. What I actually ran:

go test -ldflags=-extldflags=-Wl,-w -race \
  ./pkg/transport/session/... ./pkg/transport/proxy/transparent/...   # both ok
go vet ./test/e2e/...                                                 # clean

The flags mirror test-unixlike in the Taskfile. The go vet is there because task test filters out /test/e2e, so the e2e file this PR edits would otherwise never be compiled locally.

Not run locally: full task test, task lint-fix, task test-e2e. Leaving those to CI.

Changes

File Change
pkg/transport/session/jsonrpc_errors.go NotFoundResponse takes requestID any instead of hardcoding NotFoundBody(nil)
pkg/transport/proxy/transparent/transparent_proxy.go Hoist the parsed JSON-RPC id out of the classification block; pass it to the guard's NotFoundResponse
pkg/transport/session/jsonrpc_errors_test.go TestNotFoundResponse becomes table-driven: nil, string, raw-JSON, and nil-json.RawMessage ids
pkg/transport/proxy/transparent/revision_guard_regression_test.go TestGuardUnknownSessionFiresDespiteForgedModernRevision already sent "id":1; now asserts it comes back
test/e2e/hostile_input_proxy_test.go Delete the idNotEchoed case field, its branch, and the comment documenting this bug as known-and-deferred — the session-guard case was its only user and now echoes like every sibling

Does this introduce a user-facing change?

Yes, though a small one. A client that sends a request with an unknown or expired Mcp-Session-Id through the transparent proxy now gets its JSON-RPC id echoed in the -32001 error response instead of null, so the error can be matched to the request that caused it. Requests that genuinely carry no id — GET on the SSE stream, DELETE, or a body that isn't a single JSON-RPC request — still return "id":null.

Special notes for reviewers

A few things worth knowing:

json.RawMessage nil handling. rpcID is passed through as a json.RawMessage boxed into any. A nil one marshals to null (RawMessage.MarshalJSON special-cases the nil slice), so the no-id case is byte-identical to before. This is the same value and the same code path ClassificationErrorResponse(req, id, cerr) already takes two lines earlier, so it isn't introducing a new edge case. parseRPCRequest never returns a non-nil-but-empty id, which is the one input that would produce an invalid body. There are unit cases pinning both nil forms.

#5883 was investigated in the same pass and closed without code. It reported that a batch containing initialize skips this same guard. That turned out to be already fixed by #5931 (a18f17e2d), which added an unconditional mcp.IsBatchRequest rejection at transparent_proxy.go:591 — a 400 that lands 33 lines before the guard — and replaced the old test with TestRoundTripRejectsBatch, whose first case is verbatim the payload from that report. Closed with the details in a comment.

One thing I deliberately left alone. The batch-scanning branch in parseRPCRequest is now unreachable from RoundTrip, but it's still what would set sawInitialize = true for a batch. It's the mechanism that would resurrect #5883 if the batch rejection at :591 is ever moved or relaxed. Deleting it would make the function fail-closed, but that's a separate change from this one and I didn't want to mix it in. Flagging it for whoever edits that guard next.

🤖 Generated with Claude Code

The transparent proxy's unknown-session guard returned a -32001 JSON-RPC
error with a null id, while every sibling error path echoes the request
id, so a client correlating responses by id got null for this one error
class. Thread the parsed id through to NotFoundBody.

Closes #5945

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the size/XS Extra small PR: < 100 lines changed label Jul 27, 2026
@jhrozek

jhrozek commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Closing as a duplicate of #6031, which was opened a few minutes before this one and does the same thing better.

I missed that #6031 already existed before starting — my fault for not checking open PRs against #5945 first.

Two things #6031 gets right that this PR doesn't:

  • It updates NotFoundBody's doc comment, which still lists "transparent proxy" as a case where no id is available. That's exactly the caller that now passes one. I updated NotFoundResponse's comment and missed the one directly above it.
  • It only sets the id when parseRPCRequest reports a single request. This PR assigns it unconditionally, so a response-shaped body (id present, no method) would echo an id where Echo the request JSON-RPC id in session-not-found 404s #6031 correctly sends null.

Nothing here worth porting over. #6030 also already covers the bridge_regression_test.go build break on main that turned this PR's CI red.

@jhrozek jhrozek closed this Jul 27, 2026
@jhrozek
jhrozek deleted the fix-session-notfound-id-echo branch July 27, 2026 16:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XS Extra small PR: < 100 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Transparent proxy: session-not-found 404 does not echo the request JSON-RPC id

1 participant