Skip to content

fix(client): raise A2A errors for malformed JSON-RPC responses - #1375

Merged
Iwaniukooo11 merged 2 commits into
a2aproject:mainfrom
Sonica-B:fix/jsonrpc-client-malformed-responses
Oct 26, 2026
Merged

Iwaniukooo11 merged 2 commits into
a2aproject:mainfrom
Sonica-B:fix/jsonrpc-client-malformed-responses

Conversation

@Sonica-B

Copy link
Copy Markdown
Contributor

Description

JsonRpcTransport turned every response body into a json-rpc JSONRPC20Response (JSONRPC20Response(**response_data) in the nine unary methods, JSONRPC20Response.from_json for SSE events). That package validates the envelope with its own exceptions, so a body that is not a JSON-RPC 2.0 response object reached the caller as a raw TypeError, ValueError or json.JSONDecodeError instead of an A2AClientError:

Response body Before After
a list, or a string (e.g. from a proxy) TypeError: ... argument after ** must be a mapping A2AClientError
an object with neither result nor error ValueError: Either result or error should be used A2AClientError
"error": "boom" TypeError: ... argument after ** must be a mapping A2AClientError
error object with an extra member, e.g. {"code": -32001, "message": "...", "reason": "x"} TypeError: JSONRPCError.__init__() got an unexpected keyword argument 'reason' TaskNotFoundError
error object without message, e.g. {"code": -32001} ValueError: Error message should be string TaskNotFoundError
SSE data: that is not JSON json.JSONDecodeError (raised in the consumer loop, outside handle_http_exceptions) A2AClientError
event: error whose error object has an extra member TypeError TaskNotFoundError

The last two error-object rows matter most: the server sent a valid A2A error code, and _create_jsonrpc_error already handles a missing message (error_dict.get('message', str(error_dict))), but the json-rpc validation failed before that code ran. It is also the error-object counterpart of the spec 5.7 leniency added for results in #1354.

Change, all in src/a2a/client/transports/jsonrpc.py:

  • New _check_jsonrpc_response: an error object raises the mapped A2A error through the existing _create_jsonrpc_error; a non-object body, a non-object error, or a body with neither result nor error raises A2AClientError. A falsy error next to a result is still treated as success, as before.
  • _send_request and the SSE loop call it, so the unary methods read response_data['result'] and no longer need their own error checks. _handle_sse_error parses with json.loads and keeps its SSE stream error: ... fallback.
  • JSONRPC20Response is no longer used on the client (requests still use JSONRPC20Request). Well-formed success and error responses behave as before; the existing tests pass unchanged.

Tests: TestMalformedJsonRpcResponses in tests/client/transports/test_jsonrpc_client.py, 11 cases (unary invalid envelopes, non-standard error objects, streaming data: and event: error, plus a guard that delete_task_push_notification_config still raises on an error response). 10 of them fail on main with the exceptions in the table and pass with the change.

The v0.3 CompatJsonRpcTransport has the same JSONRPC20Response(**response_data) pattern (an error object with an extra member raises TypeError there too). I kept this PR to the 1.0 client and can send a fix(compat) follow-up.

Checks, run on Windows 11 with Python 3.10.19:

  • bash ./scripts/lint.sh: ruff check, ruff format and ty pass (three existing unused type: ignore warnings in files this PR does not touch).

  • uv run pytest tests/client/transports/test_jsonrpc_client.py: 51 passed.

  • uv run pytest --ignore=tests/integration/cross_version --ignore=tests/integration/test_scenarios.py --cov=src: 2213 passed, 199 skipped, 3 xfailed; total coverage 93%. The 3 failures (test_samples_smoke.py, console encoding) and 4 setup errors (push_notifications e2e, Can't pickle local object under Windows spawn) fail the same way on main here. tests/integration/cross_version (uses os.killpg) and tests/integration/test_scenarios.py (hangs on Windows on main too; it only builds gRPC clients) were not run locally, so the Linux CI run is the check for those.

  • Follow the CONTRIBUTING Guide.

  • Make your Pull Request title in the https://www.conventionalcommits.org/ specification.

  • Ensure the tests and linter pass

  • Appropriate docs were updated (if necessary)

JsonRpcTransport built json-rpc JSONRPC20Response objects from the raw
response body, so a body that is not a JSON-RPC 2.0 response object
leaked TypeError, ValueError or JSONDecodeError instead of an
A2AClientError, and an error object with an extra member or without a
message never reached the error-code mapping.

Validate the envelope once in _send_request and in the SSE loop, raise
the mapped A2A error straight from the error object, and raise
A2AClientError for anything else.
@Sonica-B
Sonica-B requested a review from a team as a code owner October 25, 2026 11:01
@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

🧪 Code Coverage (vs main)

⬇️ Download Full Report

Base PR Delta
src/a2a/client/transports/jsonrpc.py 92.66% 95.51% 🟢 +2.86%
Total 93.03% 93.08% 🟢 +0.05%

Generated by coverage-comment.yml

@Iwaniukooo11 Iwaniukooo11 self-assigned this Oct 26, 2026

@Iwaniukooo11 Iwaniukooo11 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Iwaniukooo11
Iwaniukooo11 merged commit 7e55413 into a2aproject:main Oct 26, 2026
17 checks passed
Iwaniukooo11 pushed a commit that referenced this pull request Oct 27, 2026
🤖 I have created a release *beep* *boop*
---


##
[1.3.0](v1.2.3...v1.3.0)
(2026-10-27)


### Features

* **server:** release parked interrupted tasks after a configurable
timeout ([#1338](#1338))
([b22a68d](b22a68d))


### Bug Fixes

* **client:** Create separate httpx per client when none configured
([#1358](#1358))
([d316d0e](d316d0e))
* **client:** raise A2A errors for malformed JSON-RPC responses
([#1375](#1375))
([7e55413](7e55413))
* **client:** rejections from unrecognized fields
([#1354](#1354))
([91929cd](91929cd))
* **compat:** keep an unset v0.3 status message and provider as None
([#1362](#1362))
([cdb4799](cdb4799)),
closes [#1361](#1361)
* **compat:** return A2A error codes from the v0.3 JSON-RPC adapter
([#1369](#1369))
([8e32d53](8e32d53))
* **compat:** set last_updated when storing a task in v0.3 format
([#1372](#1372))
([30b6d50](30b6d50)),
closes [#1371](#1371)
* **core:** restore structured __repr__ on A2A exceptions
([#1348](#1348))
([79a6f45](79a6f45))
* **server:** allow recovery after status event validation fails
([#1365](#1365))
([ca3ef0d](ca3ef0d))
* **server:** apply history_length to legacy streaming sends
([#1364](#1364))
([261c074](261c074))
* **server:** include legacy task states in database list filters
([#1337](#1337))
([e12d494](e12d494))
* **server:** record executor failures through the event consumer
([#1366](#1366))
([ad85806](ad85806))
* **server:** reject boolean JSON-RPC request IDs
([#1350](#1350))
([05392b3](05392b3))
* skip malformed agent card signature headers
([#1335](#1335))
([b21a144](b21a144))


### Documentation

* **helpers:** note that whole numbers in a data Part come back as
floats ([#1373](#1373))
([654774f](654774f))


### Miscellaneous Chores

* release 1.3.0
([#1389](#1389))
([fb6373f](fb6373f))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: mykytanetipa <mykytanetipa@google.com>
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.

2 participants