#18538 · @zaxbysauce · opened Mar 21, 2026 at 4:37 PM UTC · last updated Mar 21, 2026 at 4:38 PM UTC

fix(opencode): handle client disconnect in SSE event route writes

appfix
73
+1222 files

Score breakdown

Impact

9.0

Clarity

9.0

Urgency

9.0

Ease Of Review

9.0

Guidelines

9.0

Readiness

9.0

Size

9.0

Trust

5.0

Traction

2.0

Summary

This PR fixes a critical server stability bug where client disconnects or rapid bus events cause unhandled errors in SSE event routes. The issue leads to server corruption,

Open in GitHub

Description

Issue for this PR

Closes #15149

Type of change

  • [x] Bug fix
  • [ ] New feature
  • [ ] Refactor / code improvement
  • [ ] Documentation

What does this PR do?

The /event and /global/event SSE endpoints use an AsyncQueue drained by a for await loop that calls stream.writeSSE(). When a client disconnects abruptly — or when rapid-fire bus events cause writeSSE backpressure — the thrown error propagates out of the for await loop unhandled. This corrupts server state, causing subsequent requests to fail with "Unexpected EOF" errors and killing active sessions.

This is the same bug as the original PR #15147, rebased onto the current dev branch where the SSE routes were refactored from inline server.ts handlers into separate routes/event.ts and routes/global.ts files using the AsyncQueue pattern.

The fix wraps each writeSSE call inside the for await drain loop in a try/catch. On failure, it logs the disconnection and returns cleanly, letting the finally block run stop() to clear the heartbeat interval and unsubscribe from the bus. Two files changed, identical pattern in both.

How did you verify your code works?

  • Traced the crash path from bash.ts (fire-and-forget ctx.metadata() on every stdout chunk) through Session.updatePart()Bus.publish(PartUpdated)AsyncQueue.push()stream.writeSSE() failure → unhandled throw → session death
  • Reproduced the crash by running a test suite that produces 40+ rapid bus events through the MCP bash tool, overwhelming the SSE drain loop
  • Confirmed the fix pattern matches the existing try/catch handling already used in the server.ts inline SSE handler from the original PR

Screenshots / recordings

Not a UI change.

Checklist

  • [x] I have tested my changes locally
  • [x] I have not included unrelated changes in this PR

If you do not follow this template your PR will be automatically rejected.

Linked Issues

#15149 Server becomes unresponsive after external client disconnects during SSE streaming

View issue

Comments

No comments.

Changed Files

packages/opencode/src/server/routes/event.ts

+61
@@ -74,7 +74,12 @@ export const EventRoutes = lazy(() =>
try {
for await (const data of q) {
if (data === null) return
await stream.writeSSE({ data })
try {
await stream.writeSSE({ data })
} catch {
log.info("event disconnected (write failed)")
return
}
}
} finally {
stop()

packages/opencode/src/server/routes/global.ts

+61
@@ -113,7 +113,12 @@ export const GlobalRoutes = lazy(() =>
try {
for await (const data of q) {
if (data === null) return
await stream.writeSSE({ data })
try {
await stream.writeSSE({ data })
} catch {
log.info("global event disconnected (write failed)")
return
}
}
} finally {
stop()