Skip to content

Preserve the Request ID in Invalid Request Error Responses#400

Open
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:preserve_request_id_in_envelope_errors
Open

Preserve the Request ID in Invalid Request Error Responses#400
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:preserve_request_id_in_envelope_errors

Conversation

@koic

@koic koic commented Jun 12, 2026

Copy link
Copy Markdown
Member

Motivation and Context

Resolves #398.

JsonRpcHandler returned "id": null for JSON-RPC envelope errors (wrong or missing jsonrpc version, non-string method) even when the incoming payload carried a concrete, valid request id. From the client's perspective the original request stayed pending forever, since no response with the matching id ever arrived:

{"jsonrpc":"1.0","id":3,"method":"ping","params":{}}
=> {"jsonrpc":"2.0","id":null,"error":{"code":-32600,...}}

Per JSON-RPC 2.0 (Response object, id), the response id MUST be the same as the request's id; null is reserved for requests whose id could not be detected (e.g. Parse error). The envelope validation in process_request runs after JSON parsing with the request hash in hand, so the id is detectable - it was simply discarded by passing the :unknown_id sentinel unconditionally.

The fix passes the request's id to error_response whenever the request carries one. The existing guards keep every other case intact: error_response already nils out ids that fail validation (so a type-invalid or pattern-violating id still yields "id": null, preserving the XSS-prevention policy on echoed ids), and the :unknown_id sentinel is kept for requests without an id so an Invalid Request error still produces a null-id response, matching the spec's own example. Parse errors and non-object requests are unchanged (null is correct there), and batch entries pick up the fix automatically since each entry goes through process_request.

For reference, the TypeScript and Python SDKs currently respond with "id": null in these cases as well, because both validate the whole message against a schema (zod / pydantic) before extracting the id. This change is driven by the JSON-RPC 2.0 requirement rather than parity; the Ruby transport layer already echoes the request id where recoverable (invalid_request_response(request_id:) in StreamableHTTPTransport), and this closes the remaining gap in JsonRpcHandler.

How Has This Been Tested?

  • Reproduced the three payloads from JSON-RPC envelope errors use id:null even when the request id is present #398 before and after the fix; they now return "id":3, "id":4, and "id":8 respectively
  • New and updated unit tests covering: id preserved for wrong version, missing jsonrpc, numeric method, and rpc.-prefixed method; id preserved for an invalid entry inside a batch; null id kept for requests without an id, with an explicit null id, and with an id that fails validation

Breaking Changes

None in the protocol sense - this aligns the error responses with JSON-RPC 2.0. Clients that matched "id": null on these envelope errors will now see the request's own id, which is what allows them to correlate the error with the pending request.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

@koic koic force-pushed the preserve_request_id_in_envelope_errors branch from b2032c3 to 580016f Compare June 12, 2026 17:10
## Motivation and Context

Resolves modelcontextprotocol#398.

`JsonRpcHandler` returned `"id": null` for JSON-RPC envelope errors
(wrong or missing `jsonrpc` version, non-string `method`) even when
the incoming payload carried a concrete, valid request id.
From the client's perspective the original request stayed pending forever,
since no response with the matching id ever arrived:

```
{"jsonrpc":"1.0","id":3,"method":"ping","params":{}}
=> {"jsonrpc":"2.0","id":null,"error":{"code":-32600,...}}
```

Per JSON-RPC 2.0 (Response object, `id`), the response id MUST be
the same as the request's id; null is reserved for requests whose id
could not be detected (e.g. Parse error). The envelope validation in
`process_request` runs after JSON parsing with the request hash in
hand, so the id is detectable - it was simply discarded by passing
the `:unknown_id` sentinel unconditionally.

The fix passes the request's id to `error_response` whenever
the request carries one. The existing guards keep every other case intact:
`error_response` already nils out ids that fail validation
(so a type-invalid or pattern-violating id still yields `"id": null`,
preserving the XSS-prevention policy on echoed ids), and the `:unknown_id`
sentinel is kept for requests without an id so an Invalid Request error
still produces a null-id response, matching the spec's own example.
Parse errors and non-object requests are unchanged (null is correct there),
and batch entries pick up the fix automatically since each entry goes through
`process_request`.

For reference, the TypeScript and Python SDKs currently respond with
`"id": null` in these cases as well, because both validate the whole
message against a schema (zod / pydantic) before extracting the id.
This change is driven by the JSON-RPC 2.0 requirement rather than
parity; the Ruby transport layer already echoes the request id where
recoverable (`invalid_request_response(request_id:)` in
`StreamableHTTPTransport`), and this closes the remaining gap in
`JsonRpcHandler`.

## How Has This Been Tested?

- Reproduced the three payloads from modelcontextprotocol#398 before and after the fix;
  they now return `"id":3`, `"id":4`, and `"id":8` respectively
- New and updated unit tests covering: id preserved for wrong version,
  missing `jsonrpc`, numeric `method`, and `rpc.`-prefixed `method`;
  id preserved for an invalid entry inside a batch; null id kept for
  requests without an id, with an explicit null id, and with an id
  that fails validation

## Breaking Changes

None in the protocol sense - this aligns the error responses with
JSON-RPC 2.0. Clients that matched `"id": null` on these envelope
errors will now see the request's own id, which is what allows them
to correlate the error with the pending request.
@koic koic force-pushed the preserve_request_id_in_envelope_errors branch from 580016f to 18761cf Compare June 12, 2026 17:17
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.

JSON-RPC envelope errors use id:null even when the request id is present

1 participant