#16994: fix(gateway): prevent double terminal SSE event on OpenResponses error
gateway
stale
size: XS
Cluster:
Gateway Error Handling Improvements
## Summary
- Fix a bug where streaming error responses sent **two** terminal SSE events (`response.failed` followed by `response.completed`) instead of exactly one
- The catch block now sets `closed=true` and properly closes the SSE stream after writing `response.failed`, preventing the lifecycle event listener from triggering a second terminal event via `maybeFinalize`
## Root Cause
In `src/gateway/openresponses-http.ts`, when `agentCommand` throws during streaming mode:
1. The catch block wrote a `response.failed` SSE event
2. Then emitted a lifecycle `error` event via `emitAgentEvent`
3. The `onAgentEvent` listener caught `phase === "error"` and called `requestFinalize("failed", ...)`
4. `requestFinalize` -> `maybeFinalize` wrote a second terminal event (`response.completed` with failed status)
The OpenResponses spec expects exactly one terminal event per response stream.
## Fix
- Set `closed = true` and call `unsubscribe()` in the catch block before writing `response.failed`
- Remove the lifecycle error event emission (which was only triggering the duplicate)
- Properly close the SSE stream with `writeDone` + `res.end()` in the catch block
- The `finally` block's `!closed` guard prevents it from emitting lifecycle `end` after an error
## Test plan
- [x] All 377 existing gateway unit tests pass
- [x] Linter and formatter pass with no warnings
- [ ] Verify that streaming error responses produce exactly one `response.failed` terminal event
- [ ] Verify that normal streaming completion still produces exactly one `response.completed` terminal event
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Correctly fixes the duplicate terminal SSE event bug in streaming error responses by setting `closed=true` and calling `unsubscribe()` before writing the `response.failed` event, then properly closing the stream with `writeDone()` and `res.end()`.
**Key Changes:**
- Moved `closed = true` and `unsubscribe()` to the beginning of the catch block (before writing `response.failed`)
- Removed the lifecycle `error` event emission that was triggering the duplicate `response.completed` event via the `onAgentEvent` listener
- Added explicit stream cleanup with `writeDone(res)` and `res.end()` in the catch block
- The finally block's `!closed` guard now correctly prevents duplicate lifecycle event emission after an error
The fix ensures exactly one terminal SSE event per response stream, matching the OpenResponses specification.
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge with minimal risk - it fixes a clear spec violation with proper error handling
- The fix correctly addresses the root cause by preventing the lifecycle error event that triggered the duplicate terminal event. The logic is sound with proper guards against race conditions (early return if `closed`, initialization of `unsubscribe` to noop). All existing tests pass, and the changes align with the existing error handling patterns in the codebase.
- No files require special attention
<sub>Last reviewed commit: 9751274</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#4300: Gateway: prevent OpenAI-compatible client crash on SSE termination
by perryraskin · 2026-01-30
79.7%
#19134: fix(gateway): specify utf-8 encoding on SSE res.write() calls
by pierreeurope · 2026-02-17
78.1%
#15603: fix(gateway): correct malformed HTTP 429 response on WebSocket upgrade
by AI-Reviewer-QS · 2026-02-13
75.0%
#2114: fix(gateway): close server in canBindToHost error handler
by Episkey-G · 2026-01-26
74.9%
#22480: fix: memory leak, silent WS failures, and connection error handling
by Chase-Xuu · 2026-02-21
73.7%
#16949: fix(gateway): deliver chat:final even when sessionKey is unresolved (…
by ekleziast · 2026-02-15
72.6%
#22454: fix(macos): add re-subscribe loop to gateway stream subscribers
by mandofever78 · 2026-02-21
72.5%
#12999: feat(agents): Add streaming response metrics tracking
by trevorgordon981 · 2026-02-10
72.0%
#12983: fix(gateway): defer seq increment until after dropIfSlow filtering
by hclsys · 2026-02-10
72.0%
#19885: test(gateway,browser): isolate tests from ambient OPENCLAW_GATEWAY_...
by NewdlDewdl · 2026-02-18
71.8%