Skip to content

fix(server): release SSE streams when a Bun client disconnects - #44341

Open
sahiljagtap08 wants to merge 1 commit into
anomalyco:devfrom
sahiljagtap08:bun-sse-close
Open

fix(server): release SSE streams when a Bun client disconnects#44341
sahiljagtap08 wants to merge 1 commit into
anomalyco:devfrom
sahiljagtap08:bun-sse-close

Conversation

@sahiljagtap08

Copy link
Copy Markdown

Issue for this PR

Closes #36311

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

On the pinned Bun (1.3.14), node:http does not emit ServerResponse "close" when the client disconnects (oven-sh/bun#14697). @effect/platform-node interrupts the request fiber from exactly that event, so when a TUI drops its /api/event SSE connection the server side event stream never gets released. It keeps writing heartbeats into a dead socket, which is the state captured in the issue: server alive, listener open, health unresponsive, one core pinned in the native write path.

The fix is the bridge suggested in the issue: on Bun only, destroy the response from the request "aborted" event, which makes Bun emit the missing "close" so Effect can interrupt the stream and release its event subscription. The helper lives in core and is applied to both places we create a node:http server, the V1 server layer and the V2 serve command.

How did you verify your code works?

Added a test that opens an SSE response, aborts the client, and asserts the response "close" event fires. Without the bridge the event never fires on Bun 1.3.14 (verified with a copy of the test against a raw createServer), with the bridge it fires immediately. A second test confirms normally completed responses are not destroyed. bun test test/util/http-server.test.ts in packages/core: 2 pass. Typecheck passes for core, cli, and opencode.

Screenshots / recordings

Not a UI change.

Checklist

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

@Enough1122

Copy link
Copy Markdown

AI code review — automated review for reference, author can ignore or act on any point.

Well-scoped workaround for a nasty class of leak — hooking "aborted" and destroying the response to force Bun's missing "close" is the least invasive fix available, the comment citing the upstream issue (packages/core/src/util/http-server.ts:30-37) is much appreciated, and the process.versions.bun gate keeps Node/Electron behavior byte-identical. Both production listeners are wired (packages/cli/src/commands/handlers/serve.ts:43, packages/opencode/src/server/server.ts:201), and the tests use real sockets including the negative case (packages/core/test/util/http-server.test.ts:68-110). Two notes:

  1. Residual leak for silent disconnects. "aborted" fires on an explicit client teardown of the request, but a peer that vanishes without FIN/RST (laptop sleep, NAT drop) emits nothing, so those SSE streams still accumulate until the next response.write() fails. Periodic SSE heartbeats would self-heal this; otherwise worth adding one line to the doc comment acknowledging the bridge only covers the explicit-abort case, so future readers don't assume it's total.

  2. Coverage check on other servers. packages/stats/server/src/server.ts also builds a node:http server that runs under whatever runtime hosts it — if any of its routes are long-lived/streaming, it needs bridgeClientDisconnect too; if everything is short-request/response, fine as-is. (The WSL sidecar uses node:net, so it's unaffected.)

Minor: the writableEnded/destroyed check at http-server.ts:42 has an inherent TOCTOU window against concurrent end(), but destroying an ended response is harmless — no action needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2.0: standalone SSE disconnect can wedge Bun server at 100% CPU

2 participants