Skip to content

[Bug]: SubscribeToTask on terminal task returns TaskNotFoundError instead of UnsupportedOperationError #767

@jmesnil

Description

@jmesnil

What happened?

Summary

SubscribeToTask on a task in terminal state (TASK_STATE_COMPLETED) returns TaskNotFoundError instead of the required UnsupportedOperationError, across all three transports. On JSON-RPC and HTTP+JSON, the error is also delivered as an SSE event inside a 200 OK response instead of as an immediate error response.

Requirement

Specification

The operation enables real-time monitoring of task progress and can be used with any task that is not in a terminal state.

From Section 10.4.6 (HTTP+JSON binding):

Subscribe to task updates via streaming. Returns UnsupportedOperationError if the task is in a terminal state.

From Section 11.3.2 (REST URL patterns):

POST /tasks/{id}:subscribe - Subscribe to task updates (SSE response, returns error for terminal tasks)

Expected behavior

When SubscribeToTask is called on a task that has already reached a terminal state (completed, failed, canceled, rejected), the server MUST return an UnsupportedOperationError:

  • gRPC: UNIMPLEMENTED status code with UnsupportedOperationError details
  • JSON-RPC: Immediate JSON-RPC error response with code -32601 and error type UnsupportedOperationError
  • HTTP+JSON: Immediate HTTP 400 Bad Request with AIP-193 error body

Actual behavior

Two issues:

1. Wrong error type (all transports)

The SUT returns TaskNotFoundError instead of UnsupportedOperationError. The task exists and is accessible — it's just in a terminal state, which is a different condition from "task not found."

2. Error delivered as SSE event (JSON-RPC, HTTP+JSON)

Instead of returning an immediate error response, the SUT opens an SSE stream (200 OK with Content-Type: text/event-stream) and delivers the error as an SSE data event. This prevents clients from detecting the error via standard HTTP status codes.

Reproducer

# Step 1: Create a task that completes immediately
TASK_RESPONSE=$(curl -s -X POST http://localhost:9999/ \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":{"messageId":"tck-complete-task-test","role":"ROLE_USER","parts":[{"text":"Hello"}]}}}')
TASK_ID=$(echo "$TASK_RESPONSE" | python3 -c "import sys,json; r=json.load(sys.stdin); print(r['result']['task']['id'])")

# Step 2: Verify the task is in terminal state
curl -s -X POST http://localhost:9999/ \
  -H 'Content-Type: application/json' \
  -d "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"GetTask\",\"params\":{\"id\":\"$TASK_ID\"}}" \
  | python3 -c "import sys,json; r=json.load(sys.stdin); print(f'State: {r[\"result\"][\"status\"][\"state\"]}')"
# Output: State: TASK_STATE_COMPLETED

# Step 3: Subscribe to the terminal task
# EXPECTED: Immediate JSON-RPC error response with UnsupportedOperationError
# ACTUAL: SSE stream with TaskNotFoundError embedded as event data
curl -s -X POST http://localhost:9999/ \
  -H 'Content-Type: application/json' \
  -H 'Accept: text/event-stream' \
  -d "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"SubscribeToTask\",\"params\":{\"id\":\"$TASK_ID\"}}"
# Actual output:
#   data: {"jsonrpc":"2.0","id":3,"error":{"code":-32001,"message":"Task not found"}}
#   id: 0
#
# Issues:
#   1. Returns HTTP 200 with SSE instead of immediate JSON-RPC error response
#   2. Error code is -32001 (TaskNotFoundError) instead of -32601 (UnsupportedOperationError)

# HTTP+JSON transport shows the same issues:
curl -s -o /dev/null -w "HTTP Status: %{http_code}\n" \
  -X POST "http://localhost:9999/tasks/${TASK_ID}:subscribe" \
  -H 'Content-Type: application/json' \
  -H 'Accept: text/event-stream'
# Actual: HTTP Status: 200 (should be 400)

curl -s -X POST "http://localhost:9999/tasks/${TASK_ID}:subscribe" \
  -H 'Content-Type: application/json' \
  -H 'Accept: text/event-stream'
# Actual output:
#   : SSE stream started
#   data: {"title":"Task not found","details":"","status":404,"type":"https://a2a-protocol.org/errors/task-not-found"}
#   id: 0
#
# Issues:
#   1. Returns 200 OK with SSE instead of immediate 400 Bad Request
#   2. Error type is "task-not-found" instead of "unsupported-operation"

# gRPC transport returns error immediately but with wrong error type:
grpcurl -plaintext -d "{\"id\":\"$TASK_ID\"}" localhost:9999 lf.a2a.v1.A2AService/SubscribeToTask
# Actual output:
#   ERROR:
#     Code: NotFound
#     Message: TaskNotFoundError: Task not found
#
# Issue: Should be Code: Unimplemented with UnsupportedOperationError

TCK test

tests/compatibility/core_operations/test_task_lifecycle.py::TestSubscribeLifecycle::test_subscribe_rejects_terminal_task[grpc]
tests/compatibility/core_operations/test_task_lifecycle.py::TestSubscribeLifecycle::test_subscribe_rejects_terminal_task[http_json]
tests/compatibility/core_operations/test_task_lifecycle.py::TestSubscribeLifecycle::test_subscribe_rejects_terminal_task[jsonrpc]

Relevant log output

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    compatibilityIssue related to A2A compatibility

    Type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions