-
Notifications
You must be signed in to change notification settings - Fork 142
Description
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
- ID: STREAM-SUB-003
- Section: 3.1.6 — SubscribeToTask rejects terminal tasks
- Level: MUST
- Spec: https://github.com/a2aproject/A2A/blob/173695755607e884aa9acf8ce4feed90e32727a1/docs/specification.md#316-subscribe-to-task
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
UnsupportedOperationErrorif 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:
UNIMPLEMENTEDstatus code withUnsupportedOperationErrordetails - JSON-RPC: Immediate JSON-RPC error response with code
-32601and error typeUnsupportedOperationError - HTTP+JSON: Immediate HTTP
400 Bad Requestwith 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 UnsupportedOperationErrorTCK 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