fix(client): deliver exactly one terminal SSE callback (#1170) by chopmob-cloud · Pull Request #1173 · a2aproject/a2a-java · GitHub
Skip to content

fix(client): deliver exactly one terminal SSE callback (#1170) - #1173

Merged
ehsavoie merged 3 commits into
a2aproject:mainfrom
chopmob-cloud:fix/sse-listener-single-terminal-callback-1170
Sep 24, 2026
Merged

ehsavoie merged 3 commits into
a2aproject:mainfrom
chopmob-cloud:fix/sse-listener-single-terminal-callback-1170

Conversation

@chopmob-cloud

Copy link
Copy Markdown
Contributor

Summary

Fixes #1170. The JSON-RPC SSE listeners guarded only onComplete() with a volatile completed flag, so onError, a parse error, and post-cancellation signals could each still reach the error/completion consumer, delivering more than one terminal callback (including a null completion after an error or cancellation).

Fix

Introduce one shared atomic transition. AbstractSSEEventListener now owns an AtomicBoolean and a signalTerminal(Throwable) helper: the first caller to win compareAndSet(false, true) delivers exactly one outcome (a non-null failure or a null normal completion) and every later signal is dropped. onError, onComplete and the JSON-RPC error/parse path all route through it. The 0.3 compatibility JSON-RPC listener, which does not share the base class, mirrors the same AtomicBoolean pattern. REST is left unchanged (both native and 0.3 use a no-op completion callback, an API decision as noted in the issue).

Tests

Added coverage on both listeners for complete-then-error, error-then-complete, repeated completion, a 32-thread concurrent race, and a final event followed by completion, each asserting exactly one terminal callback. mvn -pl client/transport/jsonrpc,compat-0.3/client/transport/jsonrpc -am test passes (SSEEventListenerTest 13, SSEEventListener_v0_3_Test 12).

Note

Scoped to the terminal-callback contract in the issue. A separate, related question: the listeners do not uniformly treat a malformed frame as terminal (the native parseResponseEvent can raise an unchecked Gson JsonSyntaxException out of onMessage; the 0.3 listener logs and continues on a malformed frame). Whether a malformed frame should end the stream or be skipped is a semantics decision. Happy to follow up once you confirm the intended behaviour.

@kabir

kabir commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

chopmob-cloud and others added 2 commits September 24, 2026 17:36
The JSON-RPC SSE listeners guarded only onComplete() with a volatile
completed flag, so onError, a parse failure, and post-cancellation
signals could each still reach the error/completion consumer, delivering
more than one terminal callback (including a null completion after an
error or cancellation).

Introduce one shared atomic transition. AbstractSSEEventListener now
owns an AtomicBoolean and a signalTerminal(Throwable) helper that lets
the first caller win via compareAndSet and delivers exactly one outcome
(a non-null failure or a null normal completion); every later signal is
dropped. onError, onComplete and the parse-error path all route through
it. The 0.3 compatibility JSON-RPC listener, which does not share the
base class, mirrors the same AtomicBoolean pattern. REST is left as is
(both native and 0.3 use a no-op completion callback, an API decision).

Tests cover complete-then-error, error-then-complete, repeated
completion, a 32-thread concurrent race, and a final event followed by
completion, asserting exactly one terminal callback in each.

Signed-off-by: AlgoVoi <chopmob@gmail.com>
Signal normal completion before auto-closing streams for final events so cancellation cannot report a contradictory terminal outcome. Apply this to the shared listener and the v0.3 JSON-RPC listener.

This fixes a2aproject#1170
@kabir
kabir force-pushed the fix/sse-listener-single-terminal-callback-1170 branch from 54a25b1 to 29e414a Compare September 24, 2026 16:37
@kabir

kabir commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator

I have added a commit addressing the review comment

Verify that an onError callback caused by closing a stream after a final event does not replace the normal completion outcome in native and v0.3 JSON-RPC listeners.

This fixes a2aproject#1170
@ehsavoie
ehsavoie merged commit c257762 into a2aproject:main Sep 24, 2026
14 checks passed
@ehsavoie

Copy link
Copy Markdown
Collaborator

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.

Fix duplicate terminal callbacks in SSE listeners

3 participants