google-gemini / google-gemini/gemini-cli
bug(a2a-server): /executeCommand streaming error path writes a JSON 500 into an already-started SSE response (ERR_HTTP_HEADERS_SENT)
- Dominant language
- TypeScript
- Stars
- 107k
- Forks
- 14.6k
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 45
Description
### What happened?
In `packages/a2a-server/src/http/app.ts`, the `POST /executeCommand` handler starts an SSE response **before** running the command, but the shared `catch` block then attempts to send a JSON error response. If `commandToExecute.execute(...)` throws after streaming has begun (or even just after headers were set), the handler calls `res.status(500).json({ error })` on a response that is already committed as an event stream — producing `ERR_HTTP_HEADERS_SENT` / crashing the request, or corrupting the SSE stream with a JSON body.
```ts
expressApp.post('/executeCommand', (req, res) => {
void handleExecuteCommand(req, res, context);
});
async function handleExecuteCommand(req, res, context) {
...
if (commandToExecute.streaming) {
const eventBus = new DefaultExecutionEventBus();
res.setHeader('Content-Type', 'text/event-stream'); // <- headers prepared for SSE
const eventHandler = (event: AgentExecutionEvent) => { ... };
eventBus.on('event', eventHandler);
await commandToExecute.execute({ ...context, eventBus }, args ?? []); // <- can throw mid-stream
eventBus.off('event', eventHandler);
eventBus.finished();
return res.end();
} else { ... }
} catch (e) {
logger.error(...);
return res.status(500).json({ error: errorMessage }); // <- writes JSON into an SSE response
}
}
```
Failure modes:
1. `execute()` throws before any event is written: Express sends a 500 JSON response that still carries `Content-Type: text/event-stream` — clients parsing SSE fail.
2. `execute()` throws after one or more `data:` frames were written: `res.status(500).json()` triggers `ERR_HTTP_HEADERS_SENT` (headers/body already committed). In the worst case the thrown error inside the catch escapes `void handleExecuteCommand(...)` and becomes an unhandled rejection.
This is the same defect class already fixed for `GET /tasks/metadata` (missing early return after committing a response), but on a different endpoint and requiring a different fix shape: the streaming path needs its own error handling that emits an SSE error event (or at minimum `res.end()`) instead of writing a fresh JSON status.
Note this is distinct from PR #27754, which only adds the missing `return;` in `GET /tasks/metadata`.
### What did you expect to happen?
Once the SSE response has started, errors must be reported in-band:
```ts
if (commandToExecute.streaming) {
res.setHeader('Content-Type', 'text/event-stream');
...
try {
await commandToExecute.execute({ ...context, eventBus }, args ?? []);
} catch (e) {
eventBus.off('event', eventHandler);
logger.error(...);
// emit an in-band error frame, then close:
res.write(`data: ${JSON.stringify({ jsonrpc: '2.0', error: {...} })}\n`);
return res.end();
}
...
}
```
and/or guard the catch block with `if (!res.headersSent)` before falling back to `res.status(500)`.
### Client information
Source-level finding verified against upstream `main` at commit `5411f113c`; affects all platforms running the published `@google/gemini-cli-a2a-server` package.
### Login information
Not applicable.
### Anything else we need to know?
Sources:
- `packages/a2a-server/src/http/app.ts:160-185` — `/executeCommand` streaming path (setHeader → execute → catch writes JSON)
- `packages/a2a-server/src/http/app.ts:186-194` — shared catch block performing `res.status(500).json(...)`
- Related prior art: #27754 fixes the same "response already committed" defect class in `GET /tasks/metadata`
- Duplicate check: searched issues for "ERR_HTTP_HEADERS_SENT executeCommand" and related terms; searched open PRs touching a2a-server HTTP error handling — no existing report or fix found.
Contributor guide
Research direction
Start in packages/a2a-server/src/http/app.ts at the POST /executeCommand handler and inspect the streaming path around lines 160-185 and its shared catch around lines 186-194. Compare the response-commit handling with the related approach in issue #27754. Done means a streaming execution error is handled in-band or closes the SSE response without attempting a JSON response or causing ERR_HTTP_HEADERS_SENT.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- express, typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100