Skip to content

fix(compat-0.3): signal normal stream completion from the REST transport - #1279

Merged
kabir merged 1 commit into
a2aproject:mainfrom
WMK666666:fix-compat03-rest-stream-completion
Nov 3, 2026
Merged

kabir merged 1 commit into
a2aproject:mainfrom
WMK666666:fix-compat03-rest-stream-completion

Conversation

@WMK666666

@WMK666666 WMK666666 commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Fixes #1194.

The 0.3 REST transport drops the normal-completion signal. Both postAsyncSSE call sites in RestTransport_v0_3 (sendMessageStreaming and resubscribe) pass an empty completion callback, and RestSSEEventListener_v0_3 has no onComplete() at all. A stream that the server ends cleanly is therefore never reported to errorConsumer, and the caller cannot tell "stream finished" apart from "stream still running".

This is the compat-0.3 copy of a defect that the other three transports no longer have:

transport reports normal completion
native JSON-RPC yes
native REST yes, since #1236
compat-0.3 JSON-RPC yes (the exactly-once guard around it was added later, in #1173)
compat-0.3 REST no, this PR

Change

This is #1236 applied to the 0.3 REST code, and it also matches what the 0.3 JSON-RPC listener (SSEEventListener_v0_3) already does:

  • RestSSEEventListener_v0_3: add onComplete(), and route the existing error paths (InvalidProtocolBufferException, the invalid-payload default branch) through a new signalTerminal() that uses an AtomicBoolean to deliver exactly one terminal outcome. The body is the same as SSEEventListener_v0_3; the 0.3 REST listener has no AbstractSSEEventListener base class, so that state has to live in the class itself.
  • RestTransport_v0_3: call sseEventListener.onComplete() from the completion callback of sendMessageStreaming and resubscribe, with the same comment used by the native transport.
  • ClientTransport_v0_3: the errorConsumer Javadoc on sendMessageStreaming and resubscribe now states that a null argument signals clean completion and a non-null Throwable signals failure, matching the wording already used on the native interface.

No public API change. The terminal outcome travels through the existing errorConsumer as a null throwable, exactly the way the native and JSON-RPC transports already report it. Exposing completion as a first-class ClientTransport capability (#310) is a separate API decision and is not attempted here.

This is complementary to #1202, which keeps the in-flight futures so that close() can cancel them. Both touch the same two call sites, so whichever lands second needs a small rebase.

Tests

RestTransport_v0_3_Test gains testSendMessageStreamingSignalsNormalCompletion and testResubscribeSignalsNormalCompletion. Each asserts that errorConsumer receives a null throwable exactly once after the server ends the stream, mirroring testSendMessageStreamingCompletion on the native side.

RestSSEEventListener_v0_3_Test is new (the module had no listener-level test at all) and covers the terminal contract directly:

  • testOnErrorThenOnCompleteDeliversSingleTerminalCallback - error followed by a late completion
  • testOnCompleteThenOnErrorDeliversSingleTerminalCallback - completion followed by a late error
  • testRepeatedOnCompleteDeliversSingleTerminalCallback - three completions in a row
  • testConcurrentTerminalSignalsDeliverExactlyOneCallback - 32 threads racing completions and errors
  • testParsingFailureThenOnCompleteDeliversSingleTerminalCallback - a payload that fails to parse stays the single outcome
  • testOnCompleteDeliversNullToErrorHandler and testOnCompleteWithoutErrorHandlerDoesNotThrow

Verified locally with JDK 24:

  • mvn -pl :a2a-java-sdk-compat-0.3-client-transport-rest,:a2a-java-sdk-compat-0.3-client-transport-spi -am test - BUILD SUCCESS, 11 modules green, 19/19 across the two REST test classes.
  • Removing the two onComplete() calls in the transport still makes exactly the two transport tests fail.
  • Removing the AtomicBoolean guard in signalTerminal now makes five listener tests fail (expected: <1> but was: <2>, <3> for the repeated case, <32> for the concurrent one), so the guard is pinned rather than merely exercised.

@kabir

kabir commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

Thanks, this looks good to me. I checked the changes against #1236 and ran the affected reactor locally: all 698 tests passed. Removing the two completion calls also makes exactly the two new tests fail, as you described.

A couple of small suggestions:

  • Could you add listener tests for error followed by completion, repeated completion, and concurrent terminal signals? The current tests still pass with the atomic guard removed.
  • Could you update the errorConsumer Javadoc in ClientTransport_v0_3 to explain that null signals clean completion, matching the native interface?

One minor correction to the description: #1173 added the exactly-once guard to the 0.3 JSON-RPC listener; its completion notification already existed before that.

@WMK666666
WMK666666 force-pushed the fix-compat03-rest-stream-completion branch from 42fd35e to d2735bd Compare November 3, 2026 13:47
@WMK666666

Copy link
Copy Markdown
Contributor Author

Thanks for the review, and for running the reactor yourself.

Both suggestions are in d2735bd0:

  • Listener tests. RestSSEEventListener_v0_3_Test is new - the module had no listener-level test at all. It covers error-then-completion, completion-then-error, three completions in a row, 32 threads racing completions against errors, the parsing-failure path and the null error handler. With the AtomicBoolean guard removed, five of them fail (the counts come out as 2, 2, 3, 2 and 32), so the guard is pinned now rather than merely exercised. You were right that the previous tests passed with the guard gone.
  • Javadoc. ClientTransport_v0_3 now spells out the null-on-success convention on both sendMessageStreaming and resubscribe, using the same wording as the native interface.

You were also right about #1173 - the description is corrected: the 0.3 JSON-RPC completion notification predates it, and what #1173 added was the exactly-once guard around it.

Still a single commit, now 5 files. Local check with JDK 24: 19/19 across the two REST test classes and 11 modules green, and taking the guard out reproduces the five failures.

The 0.3 REST transport never reported a normally finished stream back to the
caller: both completion callbacks handed to postAsyncSSE were empty lambdas and
RestSSEEventListener_v0_3 had no onComplete() at all, so a client could not tell
a finished stream apart from one that is still running.

Do what the native REST transport already does since a2aproject#1236, and what the 0.3
JSON-RPC transport has always done: turn the connection-end callback into a
single terminal errorHandler(null) call. signalTerminal() keeps that to exactly
one terminal outcome, so a completion that arrives after a parsing error is
dropped instead of producing a second callback.

The listener contract is covered directly, including error-then-completion,
repeated completion and concurrent terminal signals, all of which fail when the
atomic guard is removed. The errorConsumer Javadoc in ClientTransport_v0_3 now
spells out the null-on-success convention, matching the native interface.

Fixes a2aproject#1194
@WMK666666
WMK666666 force-pushed the fix-compat03-rest-stream-completion branch from d2735bd to d34f698 Compare November 3, 2026 14:08
@WMK666666

Copy link
Copy Markdown
Contributor Author

Heads-up on the earlier run, in case the red build shows up in the timeline.

The previous run failed on QuarkusA2AJSONRPC_v0_3_VertxTest.testResubscribeNoExistingTaskError (AbstractA2AServerServerTest_v0_3:625, 10.01s), which waits for an error on a subscription to a non-existent task and never got one.

That is outside this change: what is touched here is the compat-0.3 REST client listener and transport, plus a Javadoc-only edit to ClientTransport_v0_3. The failing test lives in compat-0.3/reference/jsonrpc and drives the 0.3 JSON-RPC client, and it was green in the previous run of the same PR.

I re-ran the identical tree with no content change (d34f698c, same tree and same parent as d2735bd0), and all 14 checks are green, mergeable_state=clean. The test also passes locally in 1.4s, so the 10s latch in that test looks like the same subscribe-test timing flakiness that #903 and #1189 dealt with.

The review suggestions from before are still in this same commit.

@kabir
kabir merged commit 18694d9 into a2aproject:main Nov 3, 2026
14 checks passed
@kabir

kabir commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

Thanks @WMK666666 !

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.

[Bug]: REST transport never signals normal stream completion to the client (SSE hangs until timeout)

2 participants