fix(node:http): keep listener open in Server.closeAllConnections - #31302
fix(node:http): keep listener open in Server.closeAllConnections#31302mxschmitt wants to merge 1 commit into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughThis PR implements Server.prototype.closeAllConnections end-to-end (C++ → FFI → Rust → JS) so it closes established HTTP(S) connections but preserves the listening socket, replacing the previous behavior that shut down the listener. ChangesServer.closeAllConnections() feature
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Per the Node docs, `Server.prototype.closeAllConnections()` should forcefully close every established HTTP(S) connection but leave the listening socket alone. Bun's wrapper was calling `server.stop(true)`, which goes through `stop_listening` and tears down the listener, so any caller that relied on the documented contract — Playwright's `TestServer.reset()` between tests is the canonical case — got ECONNREFUSED on every subsequent request. Add a dedicated path through every layer parallel to the existing `closeIdleConnections` plumbing (uws → libuwsockets shim → App.rs → server host fn → class registration), then make the JS wrapper delegate to it without touching `serverSymbol`, `kConnectionsCheckingInterval`, or `listening`. Related: oven-sh#30501, oven-sh#30505 (different framing of an overlapping bug).
d8ddb13 to
d17bb1b
Compare
|
@claude review |
|
@mxschmitt apologies for the parallel PR — I opened #33394 for the same bug before finding this one, and a duplicate-detector bot pointed me here. Rather than quietly compete, here's what I found, because I think the two halves belong together. Your native primitive catches sockets mine can't. A connection that's been accepted but hasn't sent a request head has no JS wrapper, so it isn't in But routing only through uWS leaves the JS socket object stale. const serverSocket = /* captured from server.on("connection") */;
server.closeIdleConnections(); // native us_socket_close
// node: socket.destroyed === true, 'close' fired
// bun: socket.destroyed === false, 'close' never firesNode's That matters for the exact code this issue was filed about: Playwright's So I think the complete fix is both: destroy the tracked sockets through Happy to go either way:
Your issue, your PR, your call. This branch also predates the Zig → Rust server port, so it'll need a rebase either way ( |
…e listener alone Test case from #31302. Co-authored-by: Max Schmitt <max@schmitt.mx>
|
Thanks @mxschmitt, both for the report and for the fix, and sorry this sat for so long. Your analysis was right: I re-checked against a fresh Since May the server side has moved under this branch: #32488 made #35839 includes your "no connections established" test case with a |
Summary
Per the Node docs,
Server.prototype.closeAllConnections()is documented (since v18.2.0) to forcefully close every established HTTP(S) connection while leaving the listening socket open so subsequent connections can still be accepted. Bun's wrapper calledserver.stop(true), which goes throughstop_listeningand tears down the listener — so any caller that relied on the documented contract gotECONNREFUSEDon the next request.The most user-visible victim is Playwright's
TestServer.reset(), which calls_server.closeAllConnections()between tests to drop straggling sockets and assumes the listener stays up. Under Bun every test after the first failed withECONNREFUSED. With this fix the relevant Playwrighttests/page/page-click.spec.tschromium-page suite goes from 0/87 → 82/87 passing on a debug build (the remaining 3 are an unrelatedError.stackdivergence).Filed as #31301.
What changed
A dedicated
closeAllConnectionspath was added through every layer parallel to the existingcloseIdleConnectionsplumbing, then the JS wrapper was switched to delegate to it without touchingserverSymbol,kConnectionsCheckingInterval, orlistening(those belong toclose()):packages/bun-uws/src/App.h—TemplatedApp::closeAllConnections(), walkinghttpContext->getSocketGroup()->head_socketsandus_socket_close()-ing each (mirror ofcloseIdle()minus theisIdlecheck; does not touch the listener).src/uws_sys/libuwsockets.cpp—uws_app_close_all_connectionsC shim (SSL + non-SSL).src/uws_sys/App.rs—App::close_all_connectionsRust binding + extern decl.src/runtime/server/server_body.rs—close_all_connectionshost fn next toclose_idle_connections.src/runtime/server/server.classes.ts— registration.src/js/node/_http_server.ts—Server.prototype.closeAllConnectionsnow callsserver.closeAllConnections?.()and nothing else.Related
@azure/msal-nodehang). PR fix(http): preserve server reference across close() for closeAllConnections() #30505 targets that scenario but keeps the wrongserver.stop(true)call incloseAllConnections. The two fixes are complementary — this PR addresses the listener-teardown bug; fix(http): preserve server reference across close() for closeAllConnections() #30505 addresses the msal close-then-closeAllConnections sequencing. Reviewers may want to converge them.Test plan
test/js/node/http/node-http.test.ts(describe("Server.closeAllConnections")) with two cases: established keep-alive connection gets dropped and a follow-upfetchreturns 200; no-op when no connections are established.USE_SYSTEM_BUN=1 bun test ...) and pass on the debug build → load-bearing.test/js/node/test/parallel/test-http-server-close-all.js(Node parity script) exits 0.test/js/node/test/parallel/test-http-server-close-idle-wait-response.js(closeIdle parity) still exits 0.HTTP Server Security Tests - Advanced(14 tests, usescloseAllConnectionsinafterEach) all pass.