Repository navigation
fix(mcp) #5585: answer malformed JSON-RPC members instead of failing the request - #5619
Conversation
… the request The defaulting JSON accessors substitute their default only for an absent or null member, so a member that is present but of another JSON type raised out of the read. Four reads sat above the try that was supposed to answer for them: 'method' and 'params' in dispatch, whose enclosing try carries only a finally, so the raise escaped the dispatcher and reached the transport as a bodiless HTTP 500; and 'name'/'arguments' in tools/call and 'uri' in resources/read. Each read now sits under a guard that answers -32600 for a malformed request member and -32602 for a malformed params member. The catches cover both types Gson raises: IllegalStateException for an array of the wrong arity and UnsupportedOperationException for an object where a string was expected. The latter also reached prompts/get, which answered -32603 rather than -32602. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Tick the box to add this pull request to the merge queue (same as
|
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
🟢 Coverage 100.00% diff coverage · -6.21% coverage variation
Metric Results Coverage variation ✅ -6.21% coverage variation Diff coverage ✅ 100.00% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (bd0ba0d) 149149 111895 75.02% Head commit (655b2a6) 174085 (+24936) 119791 (+7896) 68.81% (-6.21%) Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#5619) 16 16 100.00% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
ReviewSolid, tightly-scoped fix with excellent commit prose and tests that were confirmed to fail beforehand. The I verified all the One unguarded JSON-RPC member left, same defect class
```java
```json throws Given the PR's stated goal - "every malformed JSON-RPC member on the MCP endpoint produce a JSON-RPC error envelope instead of failing the request" - this is an incomplete-fix gap rather than a regression, but it's the identical bug on the Minor (non-blocking)
Overall: correct, well-tested, and a clear net improvement. I'd suggest folding the |
…rpc member
isResponse runs as the first statement in dispatch, above every guard, so it is
the one member read that cannot sit under one: it decides whether a reply is
owed at all. Reading 'jsonrpc' as a string meant a payload such as
{"jsonrpc":{},"id":1} raised out of the probe and reached the transport as the
same bodiless HTTP 500 the rest of this change removes. Comparing through opt
maps any JSON shape to an object instead of demanding one, so a member that is
not the string "2.0" simply means the payload is not a response.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Review Nicely scoped fix with a clear write-up, and the tests are thoughtful - especially calling out the single-element-array coercion so the malformed path is actually exercised rather than silently coerced into a valid call. I verified the exception-type reasoning against JSONObject/Gson: getAsString() on an object throws UnsupportedOperationException, on a wrong-arity array throws IllegalStateException, so each catch (IllegalStateException | UnsupportedOperationException) is the right pair. The isResponse switch to opt(...) is also a genuine improvement (and cleaner). One gap: set_server_setting with a non-string key still reaches HTTP 500 The PR aims for every malformed JSON-RPC member to produce an envelope, but a surviving path remains in toolsCall. After the new guard reads arguments as a JSONObject, the log line calls formatArgs(toolName, args) at MCPDispatcher.java:344-345, OUTSIDE the try that starts at line 347. formatArgs does: getString("key", null) falls back only when key is absent/null. If key is present but not a string, getAsString() raises UnsupportedOperationException (object) or IllegalStateException (array), which escapes formatArgs -> escapes toolsCall -> passes through dispatch finally-only try -> bodiless HTTP 500. Exactly the defect this PR removes elsewhere. Repro (tool name must be exactly set_server_setting, since that gates the getString): Suggested fix - read key type-tolerantly so the log helper can never raise: Worth a sixth test alongside the others (tools/call set_server_setting with key={} -> -32602, not 500). Minor The two-member guards report a single combined message ("method must be a string and params an object") even when only one member is malformed. Fine for a JSON-RPC error string, just slightly less precise than the per-member messages in resourcesRead. Not worth changing. Otherwise looks good - correct, well-tested, no performance or security concerns introduced. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #5619 +/- ##
============================================
+ Coverage 66.24% 67.00% +0.75%
============================================
Files 1764 1718 -46
Lines 149149 142093 -7056
Branches 31683 30521 -1162
============================================
- Hits 98807 95208 -3599
+ Misses 37298 34211 -3087
+ Partials 13044 12674 -370 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
What does this PR do?
Makes every malformed JSON-RPC member on the MCP endpoint produce a JSON-RPC error envelope instead of failing the request.
The defaulting JSON accessors (
getString(name, default),getJSONObject(name, default)) fall back only when the member is absent or null. A member that is present but of another JSON type raises out of the read instead. Four such reads sat above thetrythat was meant to answer for them:methodindispatch-32600paramsindispatch-32600name/argumentsintoolsCall-32602uriinresourcesRead-32602name/argumentsinpromptsGet-32603-32602The
dispatchpair is the worst of them: their enclosingtrycarries only afinally(theDatabaseContextcleanup) and nocatch, so the raise left the dispatcher entirely and the transport turned it into a bodiless HTTP 500. That affects every method,pingincluded, not only the two that read anargumentsmember.methodandparamsare members of the request object, so a wrong shape there is-32600 Invalid Request.uri,nameandargumentslive insideparams, so those are-32602 Invalid params.Motivation
Closes #5585. Found while reviewing #5584, which fixed the same defect for
prompts/getonly; that fix was deliberately scoped to the surface that PR introduced, and this one covers the sites that predate it.Additional Notes
Two exception types, not one. Gson raises
IllegalStateExceptionwhengetAsString()orgetAsJsonObject()meets an array of the wrong arity, butUnsupportedOperationExceptionwhengetAsString()meets an object. Theprompts/getfix in #5584 caught only the first, which is why{"name": {}}there answered-32603. Every catch here covers both.A Gson coercion worth knowing about. A single-element array coerces:
{"method": ["ping"]}yields the string"ping"and dispatches normally. The tests therefore use{}rather than a one-element array, with a comment saying why - the obvious payload silently tests nothing.Checklist
mvn clean packagecommandFive tests added to
MCPTransportConformanceTest, which already owns envelope-level conformance (invalid ids, notifications, batching). All five were confirmed to fail before the fix: four withexpected: 200 but was: 500, and theprompts/getone withexpected: -32602 but was: -32603after temporarily reverting that catch.MCP*Testis 265 tests, 0 failures, on amvn cleanbuild.🤖 Generated with Claude Code