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.
Bug
DefaultRequestHandlerV2._setup_active_taskregisters a push-notification subscription on everySendMessagerequest that carries aSendMessageConfiguration, even when the caller did not settask_push_notification_config. Result: empty-URL rows accumulate in thepush_notification_configstable and (depending on the configuredPushNotificationSender) can throw at send time withhttpx.UnsupportedProtocol.Root cause
a2a/server/request_handlers/default_request_handler_v2.py(1.0.2) lines 215–219:Both
params.configurationandparams.configuration.task_push_notification_configare proto3 message fields, and in proto3 Python every message — including a default-constructed empty one — hasbool() == True. The condition therefore fires on everySendMessagewhose request carries anySendMessageConfiguration, regardless of whether the caller actually requested push notifications.Reproducer:
Real-world trigger
Callers commonly send
SendMessageConfiguration(return_immediately=True)to dispatch a long-running task and pick up the result later viatasks/getor push notifications they registered separately. With the current code, every such call registers an empty-URL subscription as a side effect, which then either:httpx.UnsupportedProtocol: Request URL is missing an 'http://' or 'https://' protocolon the first state transition (defaultBasePushNotificationSender).We hit (b) in production — a
tg-gateway-style consumer usingSendMessageConfiguration(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:HasFieldworks for message-typed fields andoptional-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-
urlconfigs atset_info, but the root cause sits in the handler.Environment
a2a-sdk==1.0.2DefaultRequestHandlerV2,DatabasePushNotificationConfigStoreWorkaround we shipped
gideon(downstream consumer) skip-and-warns on emptyurlin ourset_infooverride 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.