Skip to content

fix(server): return the stored id when creating a push notification config - #1236

Open
ConnorMoss02 wants to merge 5 commits into
a2aproject:mainfrom
ConnorMoss02:fix/push-config-create-returns-id
Open

ConnorMoss02 wants to merge 5 commits into
a2aproject:mainfrom
ConnorMoss02:fix/push-config-create-returns-id

Conversation

@ConnorMoss02

@ConnorMoss02 ConnorMoss02 commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The create handler returned the caller's request instead of what the store saved, so on the database store the response had no id. Neither store set task_id, so configs registered inline via SendMessage came back with task_id="".

Fix

set_info now stores a copy with task_id set and an empty id defaulted to the task id, and returns it without mutating the input. Both handlers return that. set_info now returns TaskPushNotificationConfig instead of None.

Tests

Create-handler tests over both real stores (database skipped without the sqlite extras), an inline SendMessage test, and a set_info test per store. All fail with the source change reverted.

Fixes #1237

…onfig

Both create handlers returned the caller's request object rather than what
the store persisted. The in-memory store defaults an empty id to the task id
on the caller's object, so the id survived; the database store copies first
and defaults on its private copy, so the response carried no id and reading
the config back with it failed validation.

Normalize the id in the handler before set_info so the response matches what
was stored on every backend.
@ConnorMoss02
ConnorMoss02 requested a review from a team as a code owner September 11, 2026 00:26
@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

🧪 Code Coverage (vs main)

⬇️ Download Full Report

Base PR Delta
src/a2a/server/request_handlers/default_request_handler.py 97.90% 97.92% 🟢 +0.02%
src/a2a/server/request_handlers/default_request_handler_v2.py 92.53% 92.65% 🟢 +0.12%
Total 93.06% 93.07% 🟢 +0.01%

Generated by coverage-comment.yml

@ConnorMoss02

Copy link
Copy Markdown
Contributor Author

@mykytanetipa ready for review when you have a moment. It fixes #1237.

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

general design comment

Instead of duplicating the store's ID-defaulting rule (if not params.id: params.id = task_id) across both request handlers and mutating the caller's request object, we could have PushNotificationConfigStore.set_info copy/normalize the config (including id and task_id) and return the persisted TaskPushNotificationConfig for the handler to return.

):
"""Test on_create_task_push_notification_config returns the id that was stored."""
if store_kind == 'database':
from a2a.server.tasks.database_push_notification_config_store import (

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.

sqlalchemy and aiosqlite are optional extras (a2a-sdk[sqlite]), not core dependencies in pyproject.toml.

Unlike test_database_push_notification_config_store.py, these core handler test modules run in environments without optional SQL extras.

Guard the database branch with pytest.importorskip('sqlalchemy') and pytest.importorskip('aiosqlite')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Guarded the database branch in both handler test modules with pytest.importorskip('sqlalchemy') and pytest.importorskip('aiosqlite').

task_id = cast('str', request_context.task_id)
context_id = cast('str', request_context.context_id)

if self._push_config_store and params.configuration.HasField(

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.

Not a blocker for this PR, but there is a related gap when registering a push notification config inline via SendMessage / SendStreamingMessage.

Per a2a.proto task_id is empty on SendMessageConfiguration.task_push_notification_config and id may be empty too.

Because neither this path before calling set_info nor store sets task_id or normalizes id, subsequent GetTaskPushNotificationConfig / ListTaskPushNotificationConfigs calls return a TaskPushNotificationConfig with task_id="".

We should normalize both id and task_id on this path (or in set_info) as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Covered by the set_info change, since it now sets task_id and the default id for every caller, including SendMessage / SendStreamingMessage. Added test_on_message_send_stores_inline_push_config_under_its_task in the v2 handler tests, which reads the config back with task_id and id set to the task.

@ConnorMoss02

Copy link
Copy Markdown
Contributor Author

Moved the normalization into set_info. Both stores now copy the config, set task_id, default an empty id to the task id, and return the stored copy. The handlers return that instead of params, and the caller's request is no longer mutated.

@ConnorMoss02

Copy link
Copy Markdown
Contributor Author

The failing check is test_multiserver.py::test_concurrent_followups_no_lost_update[mysql], which also failed on #1196 today. Everything else passes.

context: ServerCallContext,
) -> None:
"""Sets or updates the push notification configuration for a task."""
) -> TaskPushNotificationConfig:

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.

to avoid breaking changes for existing custom implementations: lets make return type TaskPushNotificationConfig | None.

inmemory and database stores can stay just TaskPushNotificationConfig

await self._reject_unsafe_push_url(params.url)

await self._push_config_store.set_info(
return await self._push_config_store.set_info(

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.

to avoid breaking changes for existing custom implementations: here and in legacy handler lets implement a fallback for None case (see typing comment)

smth like this:

stored = await self._push_config_store.set_info(
   task_id,
   params,
   context,
)
if stored is not None:
   return stored
fallback = TaskPushNotificationConfig()
fallback.CopyFrom(params)
fallback.task_id = task_id
if not fallback.id:
   fallback.id = task_id
return fallback

@mykytanetipa

mykytanetipa commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Regarding the test, lets retry, should pass. These are new tests related to multi-server deployment logic, some flakiness is expected (we'll implement a durable fix later).

This branch has not been deployed

No deployments
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.

Create push notification config returns no id on database-backed stores

2 participants