Request cancelled
after the client has already received a valid terminal TaskStatusUpdateEvent with final=true.
The remote A2A task completes successfully, but the client reports the normal end of the stream as an error.
## Environment
- a2a-java-sdk: 0.3.3.Final
- Transport: JSON-RPC over SSE
- HTTP client: JdkA2AHttpClient
- Java: 21
- A2A method: message/stream
- Protocol version: A2A 0.3.x
Relevant dependencies:
<dependency>
<groupId>io.github.a2asdk</groupId>
<artifactId>a2a-java-sdk-client</artifactId>
<version>0.3.3.Final</version>
</dependency>
<dependency>
<groupId>io.github.a2asdk</groupId>
<artifactId>a2a-java-sdk-client-transport-jsonrpc</artifactId>
<version>0.3.3.Final</version>
</dependency>
<dependency>
<groupId>io.github.a2asdk</groupId>
<artifactId>a2a-java-sdk-http-client</artifactId>
<version>0.3.3.Final</version>
</dependency>
## Steps to reproduce
Create an A2A client with JSON-RPC streaming enabled:
ClientConfig config = ClientConfig.builder()
.setStreaming(true)
.setPolling(false)
.build();
Client client = Client.builder(agentCard)
.withTransport(
JSONRPCTransport.class,
new JSONRPCTransportConfig(new JdkA2AHttpClient()))
.clientConfig(config)
.build();
Send a streaming message:
client.sendMessage(
message,
List.of((event, card) -> {
System.out.println("EVENT: " + event);
}),
error -> {
System.out.println("ERROR: " + error);
},
null);
The server returns normal A2A SSE events and eventually sends:
{
"jsonrpc": "2.0",
"id": "request-id",
"result": {
"taskId": "task-id",
"status": {
"state": "completed",
"timestamp": "2026-09-01T06:19:29.2982548Z"
},
"contextId": "context-id",
"final": true,
"kind": "status-update"
}
}
The server logs confirm that the task completed successfully:
Agent execution completed successfully
Task completed: signal=onComplete
However, immediately after the final event, the client error callback receives:
Request cancelled
## Actual behavior
The callback sequence is effectively:
TaskStatusUpdateEvent(final=true)
errorHandler(Request cancelled)
The application therefore sees a successfully completed A2A task as a failed streaming request.
The issue does not occur when the same endpoint is called with curl -N. curl receives the terminal event and exits normally.
## Expected behavior
After receiving:
{
"kind": "status-update",
"final": true,
"status": {
"state": "completed"
}
}
the client should complete normally.
Expected callback behavior:
TaskStatusUpdateEvent(final=true)
completion callback
The error callback should not receive Request cancelled for an internally initiated stream shutdown after a valid final event.
## Suspected root cause
SSEEventListener.handleMessage() appears to cancel the HTTP future after delivering a final status event:
eventHandler.accept(streamingEvent);
if (streamingEvent instanceof TaskStatusUpdateEvent statusUpdate
&& statusUpdate.isFinal()) {
future.cancel(true);
}
Cancelling the JDK HTTP client's future causes the HTTP layer to report:
Request cancelled
The resulting exception is then forwarded by SSEEventListener.onError():
public void onError(Throwable error, Future<Void> future) {
if (errorHandler != null) {
errorHandler.accept(error);
}
future.cancel(true);
}
The client therefore does not distinguish between:
1. an internal cancellation after receiving final=true;
2. a user-initiated cancellation;
3. an actual network or transport failure.
The completed flag also appears to be set only from onComplete(), not when a final TaskStatusUpdateEvent is received.
## Suggested fix
Mark the listener as successfully completed before cancelling the HTTP future, and suppress the cancellation exception caused by this internal
cleanup.
For example:
private volatile boolean completed;
private void handleMessage(JsonNode message, Future<Void> future) {
StreamingEventKind event = parseEvent(message);
eventHandler.accept(event);
if (event instanceof TaskStatusUpdateEvent statusUpdate
&& statusUpdate.isFinal()) {
completed = true;
future.cancel(true);
signalSuccessfulCompletion();
}
}
public void onError(Throwable error, Future<Void> future) {
if (completed && isCancellation(error)) {
return;
}
if (errorHandler != null) {
errorHandler.accept(error);
}
}
Alternatively, the client could wait for the server to close the SSE connection naturally instead of cancelling the future after receiving
final=true.
## Workaround
At the application layer, we currently ignore Request cancelled only when:
- a final event has already been received; or
- valid response artifacts have already been received.
A cancellation received before any valid response is still treated as a real error.
This workaround prevents successful A2A streaming tasks from being reported as failures, but the behavior would be better fixed inside the SDK.
Description
When using the JSON-RPC streaming client with
a2a-java-sdkversion0.3.3.Final, the error callback is invoked with: