Repository navigation
http: Server.closeIdleConnections() does not iterate pre-request sockets #63452
Description
Version
v24.15.0 (also tested on several other versions)
Platform
Darwin G0674 25.4.0 Darwin Kernel Version 25.4.0: Thu Mar 19 19:30:44 PDT 2026; root:xnu-12377.101.15~1/RELEASE_ARM64_T6000 arm64
(Also tested on Linux Mint)
Subsystem
http
What steps will reproduce the bug?
import { createServer } from "node:http";
import { createConnection } from "node:net";
const server = createServer((req, res) => res.end("ok"));
server.listen(3099, "127.0.0.1");
await new Promise((r) => server.once("listening", r));
// TCP socket that doesn't send anything
const sock = createConnection(3099, "127.0.0.1");
await new Promise((r) => sock.once("connect", r));
// Wait until the server has actually accepted and registered the socket,
await new Promise((r) => server.once("connection", r));
const closed = new Promise((r) =>
server.close(() => r(performance.now())),
);
const closeStart = performance.now();
server.closeIdleConnections();
console.log("after closeIdleConnections, awaiting close...");
const t = await Promise.race([
closed,
new Promise((r) => setTimeout(() => r(null), 2000)),
]);
console.log(t === null ? "still hung after 2s" : `closed in ${t - closeStart}ms`);
sock.destroy();How often does it reproduce? Is there a required condition?
None.
What is the expected behavior? Why is that the expected behavior?
Server.closeIdleConnections() docs say:
Closes all connections connected to this server which are not sending a request or waiting for a response.
A TCP socket that is connected but has not yet sent any HTTP data fits the description to me. So the expected behavior would be closeIdleConnections() to close them:
after closeIdleConnections, awaiting close...
closed in <some number below 2000>ms
What do you see instead?
But pre-request sockets are not closed:
after closeIdleConnections, awaiting close...
still hung after 2s
Additional information
Browsers (Chrome especially) routinely open speculative TCP connections that don't send HTTP data until needed. They remain open for a while even when the user closes the tab or navigates away from the page. Server-side, those sockets are open with bytesRead === 0 and they block graceful shutdown in pretty much all frameworks with a graceful shutdown feature:
server.close()waits for them (as it should).server.closeIdleConnections()is the documented escape hatch for "drop idle connections now," but per above it doesn't.- The only working option is
server.closeAllConnections(), which also kills in-flight requests and defeats the purpose of graceful drain.
A quick search shows that it affects graceful shutdown in frameworks. See, for instance, Fastify #5713 (closed as "not planned").
A user-space workaround exists (track 'connection' events, drop sockets with bytesRead === 0) but there's at least a mismatch between the docs and the impl.
The root cause seems to be the implementation in lib/_http_server.js that iterates this[kConnections].idle():
Server.prototype.closeIdleConnections = function closeIdleConnections() {
if (!this[kConnections]) return;
const connections = this[kConnections].idle();
for (let i = 0, l = connections.length; i < l; i++) {
if (connections[i].socket._httpMessage && !connections[i].socket._httpMessage.finished) continue;
connections[i].socket.destroy();
}
};kConnections is HTTP-level state; sockets are added when an HTTP exchange begins. Pre-request sockets exist in net.Server's connection list (server._connections) but never reach kConnections. So the HTTP-level bookkeeping doesn't see them at all.
It would be nice to see a proper Node.js-level fix, or, if it's not possible or desirable for whatever reason, a documentation update to warn against the caveat.
Activity
http: close pre-request sockets in closeIdleConnections
http: close pre-request sockets in closeIdleConnections
test(fastify): name node's close hang in the e2e cleanup comment
test(fastify): name node's close hang in the e2e cleanup comment