Skip to content

fix(client): accept an empty body when deleting a push notification config - #1445

Merged
mykytanetipa merged 2 commits into
a2aproject:mainfrom
coder-abu:fix/rest-delete-push-config-empty-body
Nov 13, 2026
Merged

mykytanetipa merged 2 commits into
a2aproject:mainfrom
coder-abu:fix/rest-delete-push-config-empty-body

Conversation

@coder-abu

Copy link
Copy Markdown
Contributor

Description

RestTransport.delete_task_push_notification_config goes through _execute_request, which always calls response.json(). The spec leaves the result to the server (3.1.10: "Confirmation of deletion (implementation-specific)"), and the SDKs answer differently:

Against a2a-js or a2a-go servers, the config is deleted, but the Python client then raises A2AClientError: JSON Decode Error. A caller that retries or reports the failure is acting on a delete that succeeded.

The delete now sends its request, checks the status, and does not read the body. Error responses still go through _handle_http_error, so a 404 with TASK_NOT_FOUND still raises TaskNotFoundError. send_http_request and the JSON-RPC transport are unchanged.

The repro at the end of this description answers the delete the way each server does. On main (2cc1dd5):

a2a-python 200 b'{}' -> ok
a2a-js     204 b''   -> A2AClientError: JSON Decode Error: Expecting value: line 1 column 1 (char 0)
a2a-go     200 b''   -> A2AClientError: JSON Decode Error: Expecting value: line 1 column 1 (char 0)

On this branch, all three print ok.

$ pytest tests/client/transports/test_rest_client.py   # src change reverted
FAILED ...::TestTaskCallback::test_delete_task_push_notification_config_ignores_body[no_content]
FAILED ...::TestTaskCallback::test_delete_task_push_notification_config_ignores_body[empty_200]
2 failed, 44 passed
$ pytest --ignore=tests/integration/cross_version   # this branch, Python 3.10, locked deps
2560 passed, 207 skipped, 4 xfailed, 1 xpassed
$ pytest --cov=src
src/a2a/client/transports/rest.py   87%   (the new lines are covered)
TOTAL   94%

The new tests also cover a mapped error and a closed client on this path. I left the cross-version tests out because their servers don't start within the 3 s timeout on my machine; the same happens on main. ruff check, ruff format and ty check pass. ty prints only the three existing unused type: ignore warnings.

#1439 touches send_http_request and the transport type hints. This change stays inside delete_task_push_notification_config to keep out of its way, apart from one added import. #1436 percent-encodes the ids in this method's path, so whichever of the two lands second has a one-line conflict there; I'll rebase this one if #1436 goes first.

No issue exists, since the new tests reproduce it.

Repro script
import asyncio

import httpx

from a2a.client.transports.rest import RestTransport
from a2a.types.a2a_pb2 import AgentCard, DeleteTaskPushNotificationConfigRequest

# How each SDK's server confirms DELETE /tasks/{id}/pushNotificationConfigs/{configId}.
SERVERS = {'a2a-python': (200, b'{}'), 'a2a-js': (204, b''), 'a2a-go': (200, b'')}


async def main() -> None:
    for name, (status, body) in SERVERS.items():
        transport = httpx.MockTransport(lambda r, s=status, b=body: httpx.Response(s, content=b))
        async with httpx.AsyncClient(transport=transport) as http:
            client = RestTransport(http, AgentCard(), 'http://agent.example')
            try:
                await client.delete_task_push_notification_config(
                    DeleteTaskPushNotificationConfigRequest(task_id='t1', id='cfg1')
                )
                print(f'{name:10} {status} {body!r:5} -> ok')
            except Exception as e:
                print(f'{name:10} {status} {body!r:5} -> {type(e).__name__}: {e}')


asyncio.run(main())

🤖 Generated with Claude Code

@coder-abu
coder-abu requested a review from a team as a code owner November 10, 2026 09:56
@github-actions

github-actions Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

🧪 Code Coverage (vs main)

⬇️ Download Full Report

Base PR Delta
src/a2a/client/transports/rest.py 89.57% 89.95% 🟢 +0.38%
Total 93.63% 93.63% ⚪️ 0.00%

Generated by coverage-comment.yml

@ishymko
ishymko requested review from a team and removed request for a team November 12, 2026 10:27
@mykytanetipa mykytanetipa self-assigned this Nov 12, 2026
…onfig

The spec leaves the result of DeleteTaskPushNotificationConfig to the
implementation. This SDK's server answers `{}`, a2a-js answers 204 No
Content and a2a-go a 200 with no body. RestTransport parsed every
success response as JSON, so against the other two servers the config
was deleted but the call raised `A2AClientError: JSON Decode Error`.

The delete now checks the status and does not read the body. Error
responses still map to A2A errors through _handle_http_error.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@coder-abu
coder-abu force-pushed the fix/rest-delete-push-config-empty-body branch from f9619f5 to 7b4082f Compare November 12, 2026 12:24

@mykytanetipa mykytanetipa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@mykytanetipa
mykytanetipa merged commit 09c9347 into a2aproject:main Nov 13, 2026
17 checks passed
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