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)

Open Beginner friendly
#29,002 0 comments 0 reactions 0 assignees View on GitHub
status/need-triage
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.