Skip to content

[Bug]:JSON-RPC streaming client reports "Request cancelled" after receiving final TaskStatusUpdateEvent #2927

Description

@skyXJJ

Description

When using the JSON-RPC streaming client with a2a-java-sdk version 0.3.3.Final, the error callback is invoked with:

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions