Repository navigation
fix(jsonrpc): handle omitted params and non-object request bodies - #1198
Conversation
JSON-RPC 2.0 allows a request to omit "params", but parseRequestBody passed the missing member down as null and every method except GetExtendedAgentCard failed with a NullPointerException. A body that is valid JSON but not an object (for example "[]") failed with an IllegalStateException from getAsJsonObject(). Neither exception is mapped by A2AServerRoutes, so both were logged at ERROR level and returned to the client as -32603 Internal error. An omitted "params" is now parsed like an empty params object, and a non-object body is rejected with a JsonMappingException, which the JSON-RPC route maps to -32600 Invalid Request. An explicit "params": null keeps its existing handling. JSON-RPC 2.0 (section 5) also requires "id" in every response, set to null when the request id could not be determined. writeJsonRpcId called nullValue(), but error responses are written with a JsonWriter that does not serialize nulls, so the member was dropped. Enable null serialization just for that value. This fixes a2aproject#1196
kabir
left a comment
There was a problem hiding this comment.
Thanks @Zhuoxi2000!
Could you also add a test in JsonUtilTest for writeJsonRpcId? The method temporarily enables writing null values so that "id": null appears in the response, then restores the writer’s previous setting.
The route tests already verify that "id": null appears. A direct test would also verify that the writer’s setting is restored afterward, so writing the ID doesn’t change how other null fields are serialized. Please cover both starting settings: null serialization enabled and disabled.
This is an optional improvement, not a blocker.
Check that "id": null is written whether or not the writer serializes nulls, and that the writer's own setting is restored afterwards, so other null members keep following it. This fixes a2aproject#1196
|
Thanks @kabir, added in 87a442e: Sanity check: with |
|
Thank you @Zhuoxi2000 |
Description
JSON-RPC 2.0 lets a request omit
params(section 4).JSONRPCUtils.parseRequestBodypassed the missing member down asnull, and every method exceptGetExtendedAgentCardhit aNullPointerExceptioninparseRequestBody(JsonElement, ...). A body that is valid JSON but not an object (for example[]) threwIllegalStateExceptionfromgetAsJsonObject().A2AServerRoutesmaps neither exception, so both ended up incatch (Throwable): the server logged an ERROR stack trace and the client got-32603 Internal error.Changes:
parseRequestBody(String, String)now rejects a non-object body with aJsonMappingException.A2AServerRoutesalready maps that to-32600 Invalid Request, the code JSON-RPC 2.0 section 5.1 defines for "not a valid Request object".parseMethodRequestparses an omittedparamslike an empty params object ({}).ListTasks(all fields optional) now succeeds withoutparams. Methods with required fields go through the same validation they already apply to"params": {}; for exampleSendMessagewithoutparamsnow returns-32602 Invalid params("Parameter 'message' may not be null"). TheGetExtendedAgentCardbranch is unchanged and still treats missing params as absent.JsonUtil.writeJsonRpcId(jsonrpc-common) now actually writes"id": null. JSON-RPC 2.0 section 5 requiresidin every response, set to null when the request id could not be determined. The method callednullValue(), but error responses are written with aJsonWriterthat does not serialize nulls, so the member was silently dropped and these error responses had noidat all. Null serialization is now enabled just for that value. This affects every error response without a known id (parse errors, Invalid Request), not only the cases in this PR.The response-parsing paths (
parseResponseEvent/parseResponseBody) are not touched.Notes on the review points from #1196:
GetExtendedAgentCard: its branch still reads the rawparamsmember, so absent parameters are handled exactly as before;testParseGetExtendedAgentCard_AbsentParamsUnchangedcovers both an omitted and an explicit-nullparams.paramsis normalized. The check is on the member being absent. An explicit"params": nullarrives asJsonNulland goes through the existing path unchanged;testParseExplicitNullParams_IsNotTreatedAsOmittedpins this."id": null(see theJsonUtilchange above). Both are asserted at the route level.JSONRPCUtils.parseResponseBodyalready rejected an error response with no usable id, and it still does. Only the message differs: it is now "Invalid 'id' type: JsonNull" instead of "Request 'id' cannot be null". Letting the client surface the error object of a null-id response would be a separate change.[]is an invalid request per the spec. A non-empty array is a valid batch in JSON-RPC 2.0, but this server does not implement batches, so it now gets-32600instead of-32603. Supporting batches would be a separate change.Tests, covering the cases requested in #1196:
JSONRPCUtilsTesttestParseOmittedParams_SameAsEmptyParams(parameterized over the 10 methods that parseparams): an omittedparamsgives the same result as"params": {}.testParseOmittedParams_ListTasksWithoutParams:ListTaskswithoutparamsparses, with non-null params.testParseOmittedParams_RequiredFieldsMissing_ThrowsInvalidParams(SendMessage,SendStreamingMessage): an omittedparamshits the existing required-field validation and throwsInvalidParamsJsonMappingExceptionwith the request id.testParseNonObjectBody_ThrowsJsonMappingException(parameterized:[], a one-element batch array, a string, a number,true,null): a plainJsonMappingExceptionis thrown.testParseExplicitNullParams_IsNotTreatedAsOmitted:ListTaskswith"params": nullis still rejected.testParseGetExtendedAgentCard_AbsentParamsUnchanged(omitted and explicit-nullparams).A2AServerRoutesTesttestOmittedParams_ReturnsInvalidParamsError:SendMessagewithoutparamsgets-32602with"id": 1.testOmittedParams_ListTasksIsDispatched:ListTaskswithoutparamsreachesJSONRPCHandler.onListTasks, and the response carries no error and"id": 1.testNonObjectBody_ReturnsInvalidRequestError(same six bodies): each one gets-32600with"id": null.The other methods with required fields (for example
GetTaskwithout anid) parse"params": {}without an error today. With this change an omittedparamsbehaves exactly the same, whichtestParseOmittedParams_SameAsEmptyParamschecks. Tightening that validation is out of scope here.Test evidence (JDK 17):
With this change, JSONRPCUtilsTest 49/49 and A2AServerRoutesTest 27/27 pass.
mvn -pl jsonrpc-common,spec-grpc,transport/jsonrpc,reference/jsonrpc,client/transport/jsonrpc -am testalso passes, including the full reference JSON-RPC server suite.With
JSONRPCUtils.javaandJsonUtil.javareverted tomain, the new tests fail:NullPointerException: Cannot invoke "com.google.gson.JsonElement.toString()" because "jsonRpc" is nullfor the omitted-params cases;IllegalStateException: Not a JSON Object: ...for each non-object body.expected: <-32600> but was: <-32603>for each of the six non-object bodies;expected: <-32602> but was: <-32603>forSendMessage;ListTasksdispatch check.With only
JsonUtil.javareverted, the six non-object route cases fail because the response has noidmember.Follow the
CONTRIBUTINGGuide.Make your Pull Request title in the https://www.conventionalcommits.org/ specification.
Ensure the tests pass
Appropriate READMEs were updated (if necessary): not needed
This fixes #1196