Skip to content

feat(server): send Authorization header in push notifications when auth is configured (Fixes #585)#1126

Open
isheng-eqi wants to merge 3 commits into
a2aproject:mainfrom
isheng-eqi:fix/push-notification-auth-585
Open

feat(server): send Authorization header in push notifications when auth is configured (Fixes #585)#1126
isheng-eqi wants to merge 3 commits into
a2aproject:mainfrom
isheng-eqi:fix/push-notification-auth-585

Conversation

@isheng-eqi

Copy link
Copy Markdown

Summary

Honor PushNotificationConfig.authentication when 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 PushNotificationConfig with authentication:

{
  "pushNotificationConfig": {
    "url": "https://client.example.com/webhook",
    "token": "notification-token",
    "authentication": {
      "schemes": ["Bearer"]
    }
  }
}

The server only sent X-A2A-Notification-Token and completely ignored the authentication field. 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, check push_info.HasField('authentication') and add an Authorization header 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_notification
  • tests/server/tasks/test_push_notification_sender.py — +4 tests (110 lines):
    • Bearer auth with credentials → Authorization: Bearer <credentials>
    • Both token + Bearer auth → both headers present
    • Empty scheme → no Authorization header
    • Empty credentials → no Authorization header

Test Plan

  • 12/12 tests pass (8 existing + 4 new)
  • ruff check + ruff format + ty check pass

Closes #585

… 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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +588 to +591
if self._producer_task and not self._producer_task.done():
self._producer_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await self._producer_task

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

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).

Suggested change
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

Comment on lines +97 to +100
if scheme and credentials:
if headers is None:
headers = {}
headers['Authorization'] = f'{scheme} {credentials}'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low

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.

Suggested change
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
  1. For consistency, prefer using conditional expressions (ternary operators) for simple assignments over if/else blocks.

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.

[Feat]: PushNotificationConfig.authentication is ignored; no Authorization header is sent in push notifications

1 participant