Handle non-object JSON-RPC messages without raising#448
Open
koic wants to merge 1 commit into
Open
Conversation
## Motivation and Context
A JSON-RPC message is an object, but two stdio-facing paths assumed that and raised
on a non-object value parsed from an incoming frame:
- `JsonRpcHandler.handle` maps each element of a batch array through `process_request`,
which reads `request[:id]` assuming a Hash. A non-object batch element (for example `[[]]`
or `[5]`) makes `[][:id]` raise `TypeError`. `handle_json` rescued only `JSON::ParserError`,
so the error escaped; over stdio it escaped the `StdioTransport#open` read loop.
The HTTP transport already rejects array bodies before dispatch, so this surfaced only on stdio.
- `MCP::Client::Stdio#read_response` parses each frame from the server and calls `parsed.key?("id")`,
which raises `NoMethodError` when the server emits a non-object frame.
Only `JSON::ParserError` was rescued, so one malformed line from the server escaped.
Both are robustness gaps: a malformed frame should be reported or skipped, not raise
an unhandled exception out of the transport.
- `process_request` now returns an `INVALID_REQUEST` error response for a non-object request
before touching `request[:id]`, so a malformed batch element becomes a normal per-element error.
- `handle_json` gains a last-resort `StandardError` rescue that returns an `INTERNAL_ERROR` response,
so an unexpected handling error still produces a JSON-RPC reply.
- `read_response` skips a non-object frame the same way it skips a frame without an id.
## How Has This Been Tested?
New tests in `test/json_rpc_handler_test.rb` cover non-object batch elements
(`[[]]`, `[5]`, `["x"]`, `[nil]`, `[true]`), a valid request followed by one non-object element,
and `handle_json("[[]]")` returning an error instead of raising. A test in
`test/mcp/server/transports/stdio_transport_test.rb` drives `open` with a `[[]]` line and
asserts it emits an error response without raising. A test in `test/mcp/client/stdio_test.rb`
has the server emit non-object frames before the response and asserts the client skips them
and still returns the response.
## Breaking Changes
None. A well-formed request, batch, or response is handled exactly as before;
only a non-object message now yields a JSON-RPC error on the server or is skipped by the client
instead of raising.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation and Context
A JSON-RPC message is an object, but two stdio-facing paths assumed that and raised on a non-object value parsed from an incoming frame:
JsonRpcHandler.handlemaps each element of a batch array throughprocess_request, which readsrequest[:id]assuming a Hash. A non-object batch element (for example[[]]or[5]) makes[][:id]raiseTypeError.handle_jsonrescued onlyJSON::ParserError, so the error escaped; over stdio it escaped theStdioTransport#openread loop. The HTTP transport already rejects array bodies before dispatch, so this surfaced only on stdio.MCP::Client::Stdio#read_responseparses each frame from the server and callsparsed.key?("id"), which raisesNoMethodErrorwhen the server emits a non-object frame. OnlyJSON::ParserErrorwas rescued, so one malformed line from the server escaped.Both are robustness gaps: a malformed frame should be reported or skipped, not raise an unhandled exception out of the transport.
process_requestnow returns anINVALID_REQUESTerror response for a non-object request before touchingrequest[:id], so a malformed batch element becomes a normal per-element error.handle_jsongains a last-resortStandardErrorrescue that returns anINTERNAL_ERRORresponse, so an unexpected handling error still produces a JSON-RPC reply.read_responseskips a non-object frame the same way it skips a frame without an id.How Has This Been Tested?
New tests in
test/json_rpc_handler_test.rbcover non-object batch elements ([[]],[5],["x"],[nil],[true]), a valid request followed by one non-object element, andhandle_json("[[]]")returning an error instead of raising. A test intest/mcp/server/transports/stdio_transport_test.rbdrivesopenwith a[[]]line and asserts it emits an error response without raising. A test intest/mcp/client/stdio_test.rbhas the server emit non-object frames before the response and asserts the client skips them and still returns the response.Breaking Changes
None. A well-formed request, batch, or response is handled exactly as before; only a non-object message now yields a JSON-RPC error on the server or is skipped by the client instead of raising.
Types of changes
Checklist