Skip to content

DefaultRequestHandlerV2._setup_active_task registers empty push subscriptions due to proto3 truthiness check #1045

@AXEG0

Description

@AXEG0

Bug

DefaultRequestHandlerV2._setup_active_task registers a push-notification subscription on every SendMessage request that carries a SendMessageConfiguration, even when the caller did not set task_push_notification_config. Result: empty-URL rows accumulate in the push_notification_configs table and (depending on the configured PushNotificationSender) can throw at send time with httpx.UnsupportedProtocol.

Root cause

a2a/server/request_handlers/default_request_handler_v2.py (1.0.2) lines 215–219:

if (
    self._push_config_store
    and params.configuration
    and params.configuration.task_push_notification_config
):
    await self._push_config_store.set_info(
        task_id,
        params.configuration.task_push_notification_config,
        call_context,
    )

Both params.configuration and params.configuration.task_push_notification_config are proto3 message fields, and in proto3 Python every message — including a default-constructed empty one — has bool() == True. The condition therefore fires on every SendMessage whose request carries any SendMessageConfiguration, regardless of whether the caller actually requested push notifications.

Reproducer:

from a2a.types.a2a_pb2 import SendMessageConfiguration, TaskPushNotificationConfig

cfg = SendMessageConfiguration()
print(bool(cfg))                                            # True
print(bool(cfg.task_push_notification_config))              # True
print(cfg.HasField("task_push_notification_config"))        # False
print(bool(TaskPushNotificationConfig()))                   # True (also unconditionally)

Real-world trigger

Callers commonly send SendMessageConfiguration(return_immediately=True) to dispatch a long-running task and pick up the result later via tasks/get or push notifications they registered separately. With the current code, every such call registers an empty-URL subscription as a side effect, which then either:

  • delivers nothing silently (if the sender no-ops on empty URL), or
  • throws httpx.UnsupportedProtocol: Request URL is missing an 'http://' or 'https://' protocol on the first state transition (default BasePushNotificationSender).

We hit (b) in production — a tg-gateway-style consumer using SendMessageConfiguration(return_immediately=True) accumulated 3 empty-URL rows in the SQLite store before we noticed.

Suggested fix

Replace the truthiness checks with HasField, the documented proto3 way to test optional message-typed presence:

if (
    self._push_config_store
    and params.HasField('configuration')
    and params.configuration.HasField('task_push_notification_config')
):
    await self._push_config_store.set_info(...)

HasField works for message-typed fields and optional-marked scalars, and the same pattern is already used elsewhere in this file (line 141: if params.HasField('page_size'):).

A defensive store implementation should additionally reject (or skip) empty-url configs at set_info, but the root cause sits in the handler.

Environment

  • a2a-sdk==1.0.2
  • Python 3.12
  • DefaultRequestHandlerV2, DatabasePushNotificationConfigStore

Workaround we shipped

gideon (downstream consumer) skip-and-warns on empty url in our set_info override and added a boot-time sweeper that GCs the table on agent restart: https://github.com/42-com/gideon/issues/39 — happy to remove both layers once this is fixed upstream.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions