fix(gateway): close WebSocket clients with 1001 on shutdown - #2553
Conversation
c4bb35d to
8e92eab
Compare
8e92eab to
df6e9fc
Compare
enisdenjo
left a comment
There was a problem hiding this comment.
there seems to be some CI tests failing, can you check them out please?
Use the disposable useServer() returns so clients get a 1001 close frame, drain the WebSocket server before awaiting the HTTP close, and bound the closing handshake by gracefulShutdownTimeout so a client that never answers cannot hold the shutdown open for ws' 30s closeTimeout.
Bun does not close WebSocket clients cleanly on shutdown. Clients see 1006, which is the symptom this branch fixes on node. Graceful HTTP shutdown does work under bun - the in-flight-request and readiness-probe tests pass there - so this is specific to the WebSocket close path. Bun's `ws` compatibility layer also reports a server-side close(1001, 'Going away') to the client as code 1000, so the close code could not be asserted there even once the close frame arrives. The force-close test keeps running on bun: it asserts the process exits, not a close code, and that holds on both runtimes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
df6e9fc to
d93020d
Compare
|
Thanks — checked, and rebased on latest Mine: the two Bun E2E jobs. My
Graceful HTTP shutdown is fine under Bun — Locally: node Not mine, and left alone:
|
|
Correction to my comment above: the close-code detail I gave was wrong, and I've fixed the PR description. I claimed Bun's So that was a red herring. The real reason is simpler: Nothing about the diff changes; the skip is still correct, and for a better reason than the one I gave. Sorry for the noise. The gap looks straightforward to close in |
Closes #2546
useServer()'s disposable was discarded and a barewsServer.close()deferred instead, which closes no clients. Nothing ever told them to go away, so a subscription only ended whenserver.closeAllConnections()destroyed the socket — which clients see as1006.It could also hang.
server.close()doesn't call back while a socket is still upgraded, andcloseAllConnections()doesn't reach upgraded sockets either, so a live subscription had nothing to end it.Contract:
1001 Going awayon SIGINT/SIGTERM.gracefulShutdownTimeoutnow also bounds the closing handshake. A client that doesn't answer is terminated when the window expires rather than after ws' 30scloseTimeout. With0, a 1s floor applies so the close frames can flush.This doesn't stagger the reconnects — graphql-ws retries
1001like any other close. That's #2554.Bun
This does not fix Bun, and cannot:
startServerForRuntimepicksstartBunServerwhenglobalThis.Bunis set, sonodeHttp.tsnever runs there. Bun serves WebSockets throughBun.serveandgraphql-ws/use/bun, and its disposer callsserver.stop()without closing WebSocket clients first. They are dropped rather than closed, so a Bun shutdown still gives clients1006- the symptom this PR removes on Node.The e2e tests that assert
1001are therefore skipped on the Bun runner. Graceful HTTP shutdown is unaffected and still covered there:should let in-flight requests complete before exitingandshould reject healthcheck and readiness probes during drainboth pass under Bun. The force-close test also still runs on Bun, since it asserts the process exits rather than a close code.Happy to follow up with the equivalent for
bun.tsif you want it - tracking the openServerWebSockets and closing them beforeserver.stop()- but it is a separate implementation and I did not want to widen this PR without asking.