Repository navigation
A2A cancelTask succeeds but the original message/stream SSE connection is never closed, leading to HTTP/2 stream exhaustion #7278
Description
🔴 Required Information
Please ensure all items in this section are completed to allow for efficient
triaging. Requests without complete information may be rejected / deprioritized.
If an item is not applicable to you - please mark it as N/A
Describe the Bug:
When an A2A client sends a task via message/stream (SSE) and later calls tasks/cancel to cancel it, the cancellation itself succeeds — the server returns the task in canceled state. However, the original message/stream SSE connection is never closed. The client never receives a terminal canceled status event on the open stream, and the server never terminates the SSE response.
Over repeated cancel cycles, these orphaned SSE connections accumulate. Eventually the HTTP/2 MAX_CONCURRENT_STREAMS limit is exceeded, and the client receives IOException: too many concurrent streams on all subsequent requests.
Steps to Reproduce:
Deploy a Google ADK Python agent (v2.8.0) with an A2A server using a2a-sdk==1.1.2 (DefaultRequestHandlerV2), with v0.3 compatibility enabled.
From an A2A client, send a task via message/stream and begin consuming the SSE response.
While the task is still running, call tasks/cancel on a separate HTTP connection.
Observe that cancelTask returns successfully with TaskState.canceled.
Observe that the original message/stream SSE connection remains open — no terminal canceled event is received, and the stream does not terminate.
Repeat steps 2–5 several times. After enough cycles, new requests fail with IOException: too many concurrent streams.
Expected Behavior:
After a successful tasks/cancel, the original message/stream SSE connection should deliver a final TaskStatusUpdateEvent with state canceled and final=true, and then close. Per the A2A specification, a task stream must terminate when the task reaches a terminal state, including canceled. The client should not need to forcefully close the SSE subscription — the server should terminate it as part of the cancellation lifecycle.
Observed Behavior:
cancelTask returns the task in canceled state — the cancellation is accepted and processed correctly.
The original message/stream SSE connection stays open indefinitely. No terminal event is delivered on it.
The Java client, expecting the server to close the stream after cancellation, holds the SSE subscription open waiting for the terminal event.
Each cancelled task leaks one HTTP/2 stream. After enough cancellations, the HTTP/2 connection pool is exhausted and all new requests fail with IOException: too many concurrent streams.
Environment Details:
ADK Library Version (pip show google-adk): google-adk[eval,mcp]==2.8.0
A2A Python SDK version: a2a-sdk==1.1.2
Desktop OS: (linux)
Python Version (python -V): 3.13-alpine
Model Information:
Are you using LiteLLM: (Yes)
Which model is being used: multiple
Client language/framework: Kotlin + Spring
A2A Java SDK:
org.a2aproject.sdk:a2a-java-sdk-compat-0.3-client:1.1.0.Final
org.a2aproject.sdk:a2a-java-sdk-compat-0.3-client-transport-jsonrpc:1.1.0.Final
Protocol: A2A v0.3 (Java compat client speaking to a2a-sdk 1.1.2 server with enable_v0_3_compat=True)
Model Information:
- Are you using LiteLLM: Yes
- Which model is being used: multiple
🟡 Optional Information
Providing this information greatly speeds up the resolution process.
Regression:
This is not a regression from a previously working state. The cancel() method in A2aAgentExecutor was previously unimplemented (raised NotImplementedError — see issue #4127). We started using cancelTask only after it became available in ADK 2.6.0. The feature appears to function correctly in isolation (the task transitions to canceled), but the side effect on the open message/stream connection is the problem.
Logs:
only unrelated logs from the stream exhaustion
How often has this issue occurred?:
- Always (100%)
Activity
chelsealong commentedon Sep 25, 2026
Following up on my earlier comment: after digging in, this isn't an ADK-side bug. ADK's
A2aAgentExecutor.cancel()already does the right thing — it enqueues a terminalTaskStatusUpdateEvent(status=CANCELED)on the event queue the a2a-sdk request handler hands it. Whether that event reaches the live SSE subscriber and closes the stream is decided entirely by a2a-python'sDefaultRequestHandlerV2/ActiveTaskmachinery.The exact symptom here matches a2aproject/a2a-python#1170 ("Cancel reports success but writes no terminal state"):
ActiveTask.cancel()used to cancel the producer task before awaiting the executor'scancel(), so the terminal-event write could race the queue teardown and get lost, leaving the SSE stream open forever. That was fixed upstream in a2aproject/a2a-python#1172 (merged), which reorders cancel to await the executor first.I reproduced this directly: a minimal
AgentExecutorshaped exactly like ADK's (enqueue one CANCELED event incancel(), slow loop inexecute()) againstDefaultRequestHandlerV2correctly closes the SSE stream on botha2a-sdk==1.1.2(the version named in this issue) and the latest1.1.5— so the fix is already present in everya2a-sdkversion installable under adk-python's owna2a-sdk>=0.3.4,<2constraint.Not opening a PR here since there's no ADK code change that would help — if you're still hitting this, double check the resolved
a2a-sdkversion in your deployment (pip show a2a-sdk) and upgrade if it's pinned below the fix.gustavomonarin commentedon Sep 25, 2026
Sounds like you got it. Missed the a2a new version. thanks.