http: close pre-request sockets in closeIdleConnections#63470
Conversation
|
Review requested:
|
2fa5144 to
95c435e
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #63470 +/- ##
==========================================
- Coverage 91.67% 90.15% -1.53%
==========================================
Files 361 718 +357
Lines 156408 227734 +71326
Branches 24050 42756 +18706
==========================================
+ Hits 143384 205304 +61920
- Misses 12751 14213 +1462
- Partials 273 8217 +7944
🚀 New features to boost your workflow:
|
pimterry
left a comment
There was a problem hiding this comment.
Thanks for opening this @semimikoh! I'd mildly prefer to do this within ConnectionList itself (see comment) as that'd be a bit cleaner, but I could be persuaded if there's a big issue with that.
| return; | ||
| } | ||
|
|
||
| const connections = this[kConnections].idle(); |
There was a problem hiding this comment.
This approach does look like it works fine, but I wonder if it's changing the wrong level. Could we just make idle() return these connections as well? That would be clearer, and avoid hitting the same issue if we use idle() anywhere else.
I think this is fairly doable: I'd suggest adding a boolean field like received_data_ in the parser, set to false in Initalize, and then true in on_message_begin. Single boolean & write, so very cheap, and then ConnectionsList::Idle can return any connections where it's still false.
What do you think?
There was a problem hiding this comment.
@pimterry
Good point, thanks — moved the fix into Parser / ConnectionsList::Idle as you suggested.
Added a received_data_ flag on Parser, reset to false in Initialize (for server-side parsers that own a ConnectionsList) and set to true in on_message_begin. ConnectionsList::Idle() now returns parsers where received_data_ is still false in addition to the existing last_message_start_ == 0 case, so pre-request sockets show up as idle and closeIdleConnections() closes them with no JS-side workaround.
received_data_ is not part of ParserComparator's ordering key, so updating it in place inside on_message_begin does not require a pop/push around the change.
Force-pushed.
Signed-off-by: semimikoh <ejffjeosms@gmail.com>
95c435e to
700ffb1
Compare
|
@metcoder95 moved the fix from JS to |
mcollina
left a comment
There was a problem hiding this comment.
I'm not convinced. If we have an incoming request with no data, we can assume some will arrive relatively soon, and we can provide a better user experience by handling that rather than killing the connection.
|
@mcollina any currently idle connection could send a request soon, whether it has sent one before or not, no? If the user calls You can try it: Start a server with Fastify (or with any other framework with graceful shutdown), visit it with Chrome once, then close the page. Then try to shut it down. Chrome's speculative connection will remain open for minutes, blocking the shutdown. The "ideal" GS sequence in my mind is, very roughly:
Steps 4 and/or 5 have to come at some point whether the connection has sent requests before or not. If not, step 6 will close them anyway. If you still disagree, maybe at least a docs update is in order? Per closeIdleConnections docs:
To me, open connections that have not yet sent a request fit the description. But they are not handled by Added data point: Not necessarily relevant, but Go's server considers pre-request connections as idle when shutting down. So do Deno and Bun, both their native server APIs and node:http emulations. |
Problem
server.closeIdleConnections()does not close TCP connections that have been accepted but have not yet sent any HTTP data, contradicting itsdocumented 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:
Before:
still hung after 2s. After: closes immediately.Reported in #63452.
Cause
New sockets are pushed into
kConnectionsfromconnectionListenerInternalviaparser.initialize(...), butParser::Initialize(
src/node_http_parser.cc) setslast_message_start_ = uv_hrtime()so thatheadersTimeout/requestTimeoutcan begin counting (DoSprotection).
ConnectionsList::Idle()only returns parsers withlast_message_start_ === 0, which becomes true only afteron_message_completeresets 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 areceived_data_flag onParser. It is reset tofalseinParser::Initialize(for server-side parsers that owna
ConnectionsList) and set totrueinon_message_begin, when llhttp signals the first byte of a new HTTP message.ConnectionsList::Idle()now also returns parsers where
received_data_is stillfalse, so accepted-but-pre-request sockets are reported as idle and closed byserver.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 ofParserComparator'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, callsserver.close()+server.closeIdleConnections(), and uses an unref'dsetTimeout(common.mustNotCall(), 1000)as the safety net: if the fix worksthe 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) andrunning the issue's repro, which closed in
~0.4msinstead 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-equivalentbehavior; CI will confirm.
Fixes: #63452