feat(server): send Authorization header in push notifications when auth is configured (Fixes #585)#1126
Conversation
… GC warning (Fixes a2aproject#1121) On the normal-completion teardown path, _run_consumer's finally block was releasing the ActiveTask reference (via _maybe_cleanup) before the producer task had finished, leaving it parked on _request_queue.get(). When GC reclaimed the ActiveTask, the still-pending producer was destroyed, triggering asyncio 'Task was destroyed but it is pending!' warnings. Fix: cancel _producer_task and await it (suppressing CancelledError) before calling _maybe_cleanup(), mirroring what cancel() already does on the explicit cancel path. This ensures deterministic teardown and prevents the GC warning.
…th is configured (Fixes a2aproject#585) The A2A protocol spec requires servers to authenticate push notifications when the client provides an authentication scheme in PushNotificationConfig. Previously BasePushNotificationSender only sent the X-A2A-Notification-Token header and completely ignored the authentication field. Now the _dispatch_notification method checks push_info.HasField('authentication') and adds an Authorization header with '{scheme} {credentials}' when both scheme and credentials are non-empty. This supports Bearer and any other scheme the client provides.
There was a problem hiding this comment.
Code Review
This pull request introduces changes to prevent asyncio pending task warnings by cancelling and awaiting the producer task before cleanup in _run_consumer. It also adds support for including an Authorization header in push notifications when authentication is configured, along with comprehensive tests for both features. The review feedback highlights a potential issue where using contextlib.suppress(asyncio.CancelledError) could inadvertently swallow the cancellation of the consumer task itself, and suggests a cleaner way to initialize the headers dictionary using a conditional expression.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if self._producer_task and not self._producer_task.done(): | ||
| self._producer_task.cancel() | ||
| with contextlib.suppress(asyncio.CancelledError): | ||
| await self._producer_task |
There was a problem hiding this comment.
Using contextlib.suppress(asyncio.CancelledError) when awaiting self._producer_task inside the finally block of _run_consumer is a known asyncio anti-pattern. If _run_consumer itself is cancelled while awaiting self._producer_task, the CancelledError raised will be the cancellation of _run_consumer itself, which will be silently swallowed by contextlib.suppress. This prevents the cancellation of _run_consumer from propagating correctly.
To avoid this, you can catch asyncio.CancelledError and only suppress it if the producer task itself was cancelled (i.e., self._producer_task.cancelled() is True).
| if self._producer_task and not self._producer_task.done(): | |
| self._producer_task.cancel() | |
| with contextlib.suppress(asyncio.CancelledError): | |
| await self._producer_task | |
| if self._producer_task and not self._producer_task.done(): | |
| self._producer_task.cancel() | |
| try: | |
| await self._producer_task | |
| except asyncio.CancelledError: | |
| if not self._producer_task.cancelled(): | |
| raise |
| if scheme and credentials: | ||
| if headers is None: | ||
| headers = {} | ||
| headers['Authorization'] = f'{scheme} {credentials}' |
There was a problem hiding this comment.
For consistency, prefer using conditional expressions (ternary operators) for simple assignments over if/else blocks. You can use a conditional expression to assign a default value to headers if it is None.
| if scheme and credentials: | |
| if headers is None: | |
| headers = {} | |
| headers['Authorization'] = f'{scheme} {credentials}' | |
| if scheme and credentials: | |
| headers = {} if headers is None else headers | |
| headers['Authorization'] = f'{scheme} {credentials}' |
References
- For consistency, prefer using conditional expressions (ternary operators) for simple assignments over if/else blocks.
Summary
Honor
PushNotificationConfig.authenticationwhen sending push notifications — the A2A protocol spec requires servers to authenticate webhook calls, but the Python SDK was silently ignoring this field.Problem
When a client provides a
PushNotificationConfigwith authentication:{ "pushNotificationConfig": { "url": "https://client.example.com/webhook", "token": "notification-token", "authentication": { "schemes": ["Bearer"] } } }The server only sent
X-A2A-Notification-Tokenand completely ignored theauthenticationfield. Webhook endpoints could not authenticate the caller, breaking the security model described in the A2A spec.Fix
In
BasePushNotificationSender._dispatch_notification, after building the token header, checkpush_info.HasField('authentication')and add anAuthorizationheader with{scheme} {credentials}when both are non-empty.Compatibility: Fully backward-compatible — when no authentication is configured, the behavior is identical (no Authorization header).
Changes
src/a2a/server/tasks/base_push_notification_sender.py— +13 lines in_dispatch_notificationtests/server/tasks/test_push_notification_sender.py— +4 tests (110 lines):Authorization: Bearer <credentials>Test Plan
ruff check+ruff format+ty checkpassCloses #585