fix: finish paused HTTP/1 parser on socket end#5364
Conversation
Signed-off-by: marko1olo <barsukdana@gmail.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a crash in the HTTP/1 client path where parser.finish() could be invoked from the socket 'end' handler while the llhttp parser was paused due to response-body backpressure, previously triggering an assert(!this.paused) and crashing the process (Fixes #5360).
Changes:
- Update
Parser.finish()to resume a paused llhttp parser before callingllhttp_finish(). - Add a regression test intended to cover the “socket end while body backpressures/pauses parsing” scenario.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| lib/dispatcher/client-h1.js | Makes Parser.finish() resilient to being called while parsing is paused by resuming llhttp first. |
| test/client.js | Adds a regression test for completing a response when the socket ends while the body causes backpressure. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let body = '' | ||
| data.body.setEncoding('utf8') | ||
| data.body.on('data', (chunk) => { | ||
| body += chunk | ||
| }) | ||
| data.body.on('end', () => { | ||
| t.strictEqual(body, payload.toString()) | ||
| }) | ||
|
|
||
| setImmediate(() => { | ||
| data.body.resume() | ||
| }) |
ronag
left a comment
There was a problem hiding this comment.
I don' think the test actually test for the fix. Please confirm that the test fails without the fix.
if (this.paused) {
|
When a response body applies backpressure, the HTTP/1 llhttp parser is left paused. If the peer then closes the socket, onHttpSocketEnd calls parser.finish(), which asserted `!this.paused` and crashed the process with an uncatchable AssertionError from the socket 'end' handler. finish() now resumes a paused parser and drains the socket through it so the response completes correctly across all body framings: - Content-Length / chunked bodies reach on_message_complete during execute() (driving the parser past the body is required; calling llhttp_finish() alone leaves them hanging). - EOF-delimited bodies (no length) can't complete via execute() and re-pause, so we resume once more and let llhttp_finish() deliver the EOF completion. - Backpressure is advisory here (onData keeps buffering delivered bytes), so we resume across pauses and loop until the socket buffer is empty, rather than parsing only a single read(). Truncated responses still surface as errors, never a false completion: short Content-Length -> UND_ERR_RES_CONTENT_LENGTH_MISMATCH, unterminated chunked -> HTTPParserError. Fixes nodejs#5360. Supersedes nodejs#5364, whose fix hangs Content-Length bodies (resume without execute) and whose test passes without the fix (the 'data' listener switches the stream to flowing mode so the parser never pauses). The tests here keep the body unconsumed until after FIN and cover Content-Length/EOF completion plus Content-Length/chunked truncation; all four fail against the unpatched parser. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes #5360.
When a response body applies backpressure, the HTTP/1 parser can be left paused. If the peer then closes the socket,
onHttpSocketEndcallsparser.finish(), which previously asserted that the parser was not paused and could crash the process from the socketendhandler.This resumes the paused llhttp parser before finishing it, then lets the existing finish path handle EOF and completion normally.
Tests:
node --expose-gc --test --test-name-pattern "socket end completes response when body is paused by backpressure" test/client.jsnpm run lint -- --no-cache lib/dispatcher/client-h1.js test/client.jsgit diff --check