a2aproject / a2aproject/a2a-tck
[Bug]: SUBSCRIBE_TO_TASK_BINDING uses POST, but the vendored spec declares GET for /tasks/{id}:subscribe
- Dominant language
- Python
- Stars
- 50
- Forks
- 40
- Avg merge
- 7d 1h
- Merged PRs (30d)
- 1
Description
### What happened?
`SUBSCRIBE_TO_TASK_BINDING` sends `SubscribeToTask` over HTTP+JSON as **POST**, but the A2A specification vendored in this repository declares that method as **GET**. As a result, the four `STREAM-SUB-*` MUST requirements cannot pass against an implementation that follows the specification.
**The TCK's own spec copy** — `specification/a2a.proto`, vendored from `a2aproject/A2A` commit `173695755607e884aa9acf8ce4feed90e32727a1` (branch `v1.0.0`), per `specification/version.json`:
```
rpc SubscribeToTask(...) {
option (google.api.http) = {
get: "/tasks/{id=*}:subscribe"
};
}
```
**The binding** — `tck/requirements/base.py:147`:
```python
SUBSCRIBE_TO_TASK_BINDING = TransportBinding(
grpc_rpc=OperationType.SUBSCRIBE_TO_TASK.value,
jsonrpc_method=OperationType.SUBSCRIBE_TO_TASK.value,
http_json_method=HTTP_POST, # ← spec says GET
http_json_path=PATH_TASK_SUBSCRIBE,
)
```
The path constant is correct (`PATH_TASK_SUBSCRIBE = "/tasks/{id}:subscribe"`, `base.py:69`); only the method differs.
### Why this looks like an oversight rather than a decision
I compared **every** HTTP+JSON binding in `tck/requirements/base.py` against the annotations in `specification/a2a.proto`. **Ten of eleven match exactly. `SubscribeToTask` is the only mismatch:**
| Operation | TCK binding | Vendored spec | |
|---|---|---|---|
| SendMessage | POST `/message:send` | `post:` | ✅ |
| SendStreamingMessage | POST `/message:stream` | `post:` | ✅ |
| GetTask | GET `/tasks/{id}` | `get:` | ✅ |
| ListTasks | GET `/tasks` | `get:` | ✅ |
| CancelTask | POST `/tasks/{id}:cancel` | `post:` | ✅ |
| **SubscribeToTask** | **POST** `/tasks/{id}:subscribe` | **`get:`** | ❌ |
| CreatePushNotificationConfig | POST `/tasks/{id}/pushNotificationConfigs` | `post:` | ✅ |
| GetPushNotificationConfig | GET `…/{configId}` | `get:` | ✅ |
| ListPushNotificationConfigs | GET `…/pushNotificationConfigs` | `get:` | ✅ |
| DeletePushNotificationConfig | DELETE `…/{configId}` | `delete:` | ✅ |
| GetExtendedAgentCard | GET `/extendedAgentCard` | `get:` | ✅ |
Notably `SendStreamingMessage` is POST **because the spec says `post:`** — so "streaming implies POST" isn't the rule being applied; the spec is followed everywhere except here.
### Impact
Four **MUST**-level requirements are unpassable for a spec-conformant SUT:
- `STREAM-SUB-001` — SubscribeToTask returns Task as first event
- `STREAM-SUB-002` — SubscribeToTask stream terminates at terminal state
- `STREAM-SUB-003` — SubscribeToTask rejects terminal tasks
- `STREAM-SUB-004` — SubscribeToTask returns TaskNotFoundError for invalid task
Our server implements `GET /tasks/{id}:subscribe` per the spec and correctly returns **405 Method Not Allowed** for POST. The TCK reports this as a MUST failure. Because these are MUST-level, a release gate keyed on MUST results is held shut by a correct implementation — and the obvious "fix" on our side would be to accept POST, i.e. to become **non-conformant to the specification in order to pass the conformance suite**.
### Versions affected
- `1.0.0.alpha2` (`29063fe95e903cddac5d8ff811ab94df1ad6ef86`) — the tag we pin
- `main` (`5996b79f9cefa6fc390980e383e358a66fb9e49e` at time of writing) — **still `HTTP_POST`**, so upgrading does not resolve it
### Suggested fix
```diff
SUBSCRIBE_TO_TASK_BINDING = TransportBinding(
grpc_rpc=OperationType.SUBSCRIBE_TO_TASK.value,
jsonrpc_method=OperationType.SUBSCRIBE_TO_TASK.value,
- http_json_method=HTTP_POST,
+ http_json_method=HTTP_GET,
http_json_path=PATH_TASK_SUBSCRIBE,
)
```
`tck/transport/http_json_client.py:296` also documents the POST behaviour in its docstring (`"""Subscribe to task updates via ``POST /tasks/{id}:subscribe``."""`) and would want updating alongside. I have not checked whether `_request_streaming` needs changes to issue a bodyless GET — that's the one part of this I can't assert.
### Scope of what I verified
- I checked only the **HTTP+JSON** bindings. I did not evaluate the gRPC or JSON-RPC mappings, so I can't say whether they're affected.
- This appears **related to but distinct from #138** ("Normalize A2A method mapping source across JSON-RPC transport and validators"), which concerns JSON-RPC method *names*. This is an HTTP verb in the HTTP+JSON binding. If you'd prefer to fold it into #138 as one "mapping sources disagree with the spec" theme, that seems reasonable — they share a root cause in mappings being maintained separately from the spec they encode.
- A possible systemic follow-up: since the spec is vendored in-repo with its HTTP annotations, the bindings could be validated against `specification/a2a.proto` in a unit test, which would catch this class of drift automatically. Happy to open a PR for the one-line fix, the docstring, and/or that check if it would be useful — just say which you'd want.
### Relevant log output
```shell
FAILED tests/compatibility/core_operations/test_requirements.py::test_must_requirement[STREAM-SUB-004-http_json]
AssertionError: STREAM-SUB-004 [SubscribeToTask returns TaskNotFoundError for invalid taskId]
failed on http_json: Expected error code 404 (TaskNotFoundError), got 405
# SUT implements the spec's `get: "/tasks/{id=*}:subscribe"`, so POST is rejected:
$ curl -i -X POST https:///a2a/v1/tasks/does-not-exist:subscribe
HTTP/1.1 405 Method Not Allowed
$ grep -n "http_json_method" tck/requirements/base.py | sed -n '6p'
150: http_json_method=HTTP_POST,
$ grep -n 'subscribe' specification/a2a.proto
get: "/tasks/{id=*}:subscribe"
```
### Code of Conduct
- [X] I agree to follow this project's Code of Conduct
Contributor guide
Assessment
This issue has not been assessed yet.