Skip to content

fix: unify JSON-RPC message validation policy across transports#212

Merged
benbrandt merged 1 commit into
mainfrom
fix/unify-transport-validation
Jul 6, 2026
Merged

fix: unify JSON-RPC message validation policy across transports#212
benbrandt merged 1 commit into
mainfrom
fix/unify-transport-validation

Conversation

@benbrandt

Copy link
Copy Markdown
Member

Fixes #211

Problem

Message validation differed by transport. The stdio path (ndJsonStream) skips only non-object JSON lines and lets the connection layer handle everything object-shaped — which it does robustly since #210 (dispatches lenient requests, fast-rejects structurally invalid responses with RequestError.invalidRequest). The WebSocket and SSE paths instead validated with the strict isJsonRpcMessage guard and silently dropped anything that failed it:

  • A lenient peer's response (e.g. {"jsonrpc":"2.0","id":1,"result":{...},"error":null}) was dropped, so the caller's sendRequest promise hung until connection close — there is no request timeout.
  • A request missing the jsonrpc field got neither processing nor the spec-mandated -32600 error response.

The same messages worked over stdio.

Fix

Relax the per-transport guards to the shared isRecord object check and let Connection own the accept/reject policy in one place:

  • ws-stream.ts (client) and sse.ts: non-object payloads are skipped with a warning; object-shaped messages are forwarded to the connection layer. Also removes ws-stream.ts's private duplicate of isRecord.
  • ws-server.ts: same relaxation post-initialization; the pre-initialization handshake still strictly requires a valid initialize request, and the batch-array rejection is unchanged.
  • server.ts (HTTP POST): non-object bodies still get 400; object-shaped messages — including client responses to server-initiated requests, which previously 400'd and hung the server-side request — are forwarded.

Non-object payloads remain a transport-level rejection everywhere because they carry no id to answer and would previously have thrown on the in operator (hardened in #210's receiveMessage guard as a second line of defense).

Tests

Updated the WS stream filtering test to assert lenient objects pass through (and clarified that byte frames were always ignored by the browser-style fake socket — the old single-read assertion couldn't distinguish), and added SSE pass-through and non-object-skip tests. Full npm run check passes (634 tests).

🤖 Generated with Claude Code

The WebSocket and SSE receive paths validated messages with the strict
isJsonRpcMessage guard and silently dropped anything that failed it, so
a lenient peer's response (e.g. error:null alongside result) or request
(e.g. missing the jsonrpc field) never settled the caller's promise —
the same shapes worked over stdio, which only skips non-object lines.

Relax the per-transport guards (ws-stream, ws-server, sse, HTTP server
POST) to an object check and let the connection layer own the
accept/reject policy: it dispatches lenient requests, fast-rejects
structurally invalid responses with RequestError.invalidRequest, and
logs anything else, as of #210. Non-object payloads are still rejected
at the transport with a warning (or HTTP 400) since they carry no id to
answer. Also consolidates ws-stream's private isRecord duplicate.

Fixes #211

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@benbrandt benbrandt merged commit 5e01eb2 into main Jul 6, 2026
6 checks passed
@benbrandt benbrandt deleted the fix/unify-transport-validation branch July 6, 2026 12:47
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.

Unify JSON-RPC message validation policy across transports

1 participant