Title: http: close pre-request sockets in closeIdleConnections by semimikoh · Pull Request #63470 · nodejs/node · GitHub
Open Graph Title: http: close pre-request sockets in closeIdleConnections by semimikoh · Pull Request #63470 · nodejs/node
X Title: http: close pre-request sockets in closeIdleConnections by semimikoh · Pull Request #63470 · nodejs/node
Description: Problem server.closeIdleConnections() does not close TCP connections that have been accepted but have not yet sent any HTTP data, contradicting its documented behavior ("Closes all connections connected to this server which are not sending a request or waiting for a response") and blocking graceful shutdown when peers (e.g. browsers opening speculative connections) hold sockets open without writing. Repro: 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)); const sock = createConnection(3099, '127.0.0.1'); await new Promise((r) => sock.once('connect', r)); await new Promise((r) => server.once('connection', r)); const closed = new Promise((r) => server.close(() => r(performance.now()))); const closeStart = performance.now(); server.closeIdleConnections(); 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(); Before: still hung after 2s. After: closes immediately. Reported in #63452. Cause New sockets are pushed into kConnections from connectionListenerInternal via parser.initialize(...), but Parser::Initialize (src/node_http_parser.cc) sets last_message_start_ = uv_hrtime() so that headersTimeout / requestTimeout can begin counting (DoS protection). ConnectionsList::Idle() only returns parsers with last_message_start_ === 0, which becomes true only after on_message_complete resets it. Sockets that were accepted but have not sent any HTTP data are therefore classified as active and skipped by closeIdleConnections(). Fix src/node_http_parser.cc: add a received_data_ flag on Parser. It is reset to false in Parser::Initialize (for server-side parsers that own a ConnectionsList) and set to true in on_message_begin, when llhttp signals the first byte of a new HTTP message. ConnectionsList::Idle() now also returns parsers where received_data_ is still false, so accepted-but-pre-request sockets are reported as idle and closed by server.closeIdleConnections(). Per @pimterry's review suggestion, the fix lives at the parser layer rather than as a JS-side filter, so the semantics of idle() are correct for any current or future caller. received_data_ is not part of ParserComparator's ordering key, so it can be updated in place without pop/push around the std::set. Test Added test/parallel/test-http-server-close-idle-connections-pre-request.js. It opens a TCP connection without sending any data, calls server.close() + server.closeIdleConnections(), and uses an unref'd setTimeout(common.mustNotCall(), 1000) as the safety net: if the fix works the event loop drains and the timer never fires; without the fix the timer fires and the assertion trips. Verification The behavior change was first validated by reproducing the fix's semantics in JS (returning pre-request sockets from idle() equivalent) and running the issue's repro, which closed in ~0.4ms instead of hanging. A full local build of the patched binary was not completed because this machine has Apple clang 16.0.0, while the current tree requires a newer toolchain (V8 fails to compile due to missing std::atomic_ref). The C++ change is small and the semantics are 1:1 with the validated JS-equivalent behavior; CI will confirm. Fixes: #63452
Open Graph Description: Problem server.closeIdleConnections() does not close TCP connections that have been accepted but have not yet sent any HTTP data, contradicting its documented behavior ("Closes all connections...
X Description: Problem server.closeIdleConnections() does not close TCP connections that have been accepted but have not yet sent any HTTP data, contradicting its documented behavior ("Closes all connect...
Opengraph URL: https://github.com/nodejs/node/pull/63470
X: @github
Domain: github.com
| route-pattern | /:user_id/:repository/pull/:id/commits/:range(.:format) |
| route-controller | pull_requests |
| route-action | commits |
| fetch-nonce | v2:087c0748-f019-443a-ced8-5b58e1736f20 |
| current-catalog-service-hash | ae870bc5e265a340912cde392f23dad3671a0a881730ffdadd82f2f57d81641b |
| request-id | D3C2:A957D:4B9B79C:651A401:6A65578C |
| html-safe-nonce | 7873211f2933a13d7f8c73918ef58dc738f14e9617826292aeb2e070009ee348 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJEM0MyOkE5NTdEOjRCOUI3OUM6NjUxQTQwMTo2QTY1NTc4QyIsInZpc2l0b3JfaWQiOiIzNjg5MDM5Mzk3NjM0MDA0ODc2IiwicmVnaW9uX2VkZ2UiOiJpYWQiLCJyZWdpb25fcmVuZGVyIjoiaWFkIn0= |
| visitor-hmac | 380e96a5c6dc4a2bfbefe6124f263a42a506ecd7ba2f4b157deb78ea402c3883 |
| hovercard-subject-tag | pull_request:3720910895 |
| github-keyboard-shortcuts | repository,pull-request-list,pull-request-conversation,pull-request-files-changed,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/nodejs/node/pull/63470/commits/700ffb1ff5d4f936c29d63b55cc2ea19f0f104df |
| twitter:image | https://avatars.githubusercontent.com/u/123785695?s=400&v=4 |
| twitter:card | summary_large_image |
| og:image | https://avatars.githubusercontent.com/u/123785695?s=400&v=4 |
| og:image:alt | Problem server.closeIdleConnections() does not close TCP connections that have been accepted but have not yet sent any HTTP data, contradicting its documented behavior ("Closes all connections... |
| og:site_name | GitHub |
| og:type | object |
| hostname | github.com |
| expected-hostname | github.com |
| None | 52c76df668885aaff23b50bdca1fa1ea44ac9c1553e888ebc70ff1e4daa4625b |
| turbo-cache-control | no-preview |
| diff-view | unified |
| go-import | github.com/nodejs/node git https://github.com/nodejs/node.git |
| octolytics-dimension-user_id | 9950313 |
| octolytics-dimension-user_login | nodejs |
| octolytics-dimension-repository_id | 27193779 |
| octolytics-dimension-repository_nwo | nodejs/node |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 27193779 |
| octolytics-dimension-repository_network_root_nwo | nodejs/node |
| turbo-body-classes | logged-out env-production page-responsive full-width |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 309153364422b3c499922d1a2a6404910a58ed8e |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width