Skip to content

node:http: a request gets its body behind the FIN of socket.end() - #44419

Merged
dylan-conway merged 8 commits into
mainfrom
robobun/c5aec4a7/upgrade-body-half-open
Oct 22, 2026
Merged

dylan-conway merged 8 commits into
mainfrom
robobun/c5aec4a7/upgrade-body-half-open

Conversation

@robobun

@robobun robobun commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes a node:http regression: a handler that rejects an upload early and then half-closes the socket never sees its request end.

http.createServer((req, res) => {
  req.on("data", () => {}).on("end", () => console.log("req end")).on("close", () => console.log("req close"));
  res.writeHead(401, { "content-length": 2 }); res.end("no");
  req.socket.end();
});

A raw client sends the POST head with Content-Length: 1000 and sends the body once it has seen the server's FIN.

req events body bytes
Node v22.22.0 end, close 1000
Bun 1.4.2 end, close 0
Bun 1.4.3, main none, ever 0
this PR end, close 1000

Cause. HttpContext::onData drops every read of a socket that is shut down, unless it is a tunnel. An http.Server socket has allowHalfOpen, so socket.end() only ends the write side. Since #43557 a request ends at its real last chunk, and that chunk is never read. The response has finished, so the close of the socket does not abort the request either.

Fix.

  • socket.end() marks the connection (HTTP_NODE_HALF_CLOSED, set in deferShutdownUntilResponseDrains). The three shut-down checks of HttpContext::onData let the body of the request in flight through (readsBodyBehindOwnFin). The last chunk stops the parse as before, so no other request is read behind the FIN.
  • HttpParser::hasIncompleteRequestBody() no longer counts a chunked body whose last chunk was delivered. A data handler that stops the parse there leaves STATE_IS_TRAILERS_DONE. Without this, https reports HPE_INVALID_EOF_STATE for a complete body and the connection never counts as idle.
  • onDataIncomingMessage: behind the FIN, a reader that is slower than the client pauses the reads like on any other connection. A body that nothing reads is dropped from the next read on, so the client's FIN still closes the connection (the existing server.close() test in this file needs that). The new finSent getter tells "the FIN has left" from "end() was called and the FIN waits for queued bytes", where nothing changes.
  • socket.destroySoon() goes through the same code. The request now gets the body that came in the read of its head, as in Node. The socket still closes behind that read.

Source change against main: +35/-8 lines.

Unchanged, and still different from Node:

  • socket.end() while response bytes are still queued (a response larger than the send buffer, or a client that does not read). With a finished response the connection closes at the flush and the body is lost. node:http: half-close from the drain gate when socket.end() waited for buffered bytes #44244 covers the case of an unfinished response. This PR no longer builds on it.
  • The body of an accepted Upgrade request is not read behind the FIN (node:http: an Upgrade request with a body follows Node v26 #45058).
  • A request that arrives behind the FIN is not dispatched. Node dispatches it, and its response cannot leave.
  • Over plain http, the peer's FIN behind ours closes the socket with no socket 'end' and no HPE_INVALID_EOF_STATE for a body that was cut short. https reports the cut as on main.
  • A reader that attaches after the buffer of req filled behind the FIN gets only the buffered part. Node pauses the reads and gives it all.

Changed as a consequence: a handler that ends the socket and never ends res (req.socket.end("HTTP/1.1 400 ...")). On main requestTimeout closes that connection, because the body is never read. Now the request completes, so only the client's FIN closes it, as in Node. With res.end() the keep-alive timeout applies as usual.

How did you verify your code works?

test/js/node/http/node-http-server-socket-end-drain.test.ts: 36 new tests, over http and https.

  • 32 fail on a debug build of main's sources. The other 4 are guards for behavior that must not change: the unread body of an Upgrade request, and a late reader while the FIN waits.
  • All 45 tests of the file pass on this branch (debug build with ASAN, Linux x64).
  • Each clause of the fix was removed in turn. Every removal fails at least one test, except the HTTP_CONNECTION_SCOPED entry. That one only matters when a request is dispatched between end() and a FIN that a pending TLS write delays.
  • 35 scenarios, the script above among them, ran under Node v22.22.0, main's sources and this branch, over http and https: Content-Length and chunked bodies, a body that starts in the read of the head, a body that is cut short, a malformed chunk, requestTimeout on a stalled body, server.close(), pipelined requests, CONNECT, Upgrade, Connection: close. What req emits and reads matches Node in every rejection scenario. The scenarios listed as unchanged give the same output as main.

Generated by Claude Code

…r buffered bytes

The native end() of the server socket defers its shutdown while uWS holds
unsent bytes. For destroySoon() the drain gate of HttpContext::onWritable
closed the connection. For a plain end() on a response that never ends, no
arm of the gate matched, so the FIN was never sent.

The deferral now sets HTTP_NODE_SHUTDOWN_AFTER_DRAIN. When the bytes are
out and a response is still in flight, the gate calls
JSNodeHTTPServerSocket::halfClose(): the FIN goes out and the socket stays
until the peer ends. If the peer ended first, the socket closes behind the
FIN.

The connection reads behind that FIN as it did before it:

- HttpContext::onData keeps parsing, so the rest of a request body reaches
  the request.
- The loop reports the FIN of the peer to HttpContext::onEnd before it
  closes the socket (us_socket_t::end_after_shutdown). The socket emits
  'end', and a request that the FIN cuts gets its 'clientError'.
- A pause of flood prevention ends at the FIN. Unsent bytes and the queue
  of responses can no longer end it. A request body that paused the reads
  keeps its pause.

A response gets no 'drain' once its socket has ended, as in Node. A writer
that continues on 'drain' can then no longer hold the FIN back.
… the one that waits

The connection reads on behind the FIN of every socket.end(). A request
gets the body that arrives there, and an Upgrade request with a body
becomes a tunnel when that body ends. A body that nothing reads is
dropped when the buffer of the request is full, so that the peer's FIN
closes the connection.
@github-actions github-actions Bot added the claude label Oct 2, 2026
@robobun

robobun commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

This pull request is a draft while it is reworked. Its current head (ac65334ac9) is stale: it is stacked on #44244, and a check found that it can reset a connection whose response is still queued.

The rework is narrower. It keeps one rule: on a node:http server, socket.end() does the same thing whether or not bytes are still queued, only later. The FIN follows the queued bytes, also when the response never ends, and no close follows that FIN. server.close() completes for a connection behind its own FIN. The change of #44244 is folded in, and the base becomes main.

Out of this pull request: reads behind the FIN and the Upgrade request with a body (#45058 covers that), and the request timeout window behind the FIN.

I will replace this comment with the reproduction and the measured outputs when the rework is pushed.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟡 src/jsc/bindings/node/JSNodeHTTPServerSocket.cpp — A handler that paused a request body and then calls socket.end() receives up to one extra 512 KB read pushed past the 16 KB highWaterMark, where the base delivered nothing after the FIN. halfClose at src/jsc/bindings/node/JSNodeHTTPServerSocket.cpp:391-401 calls us_socket_pause then us_socket_resume unconditionally for the immediate path (keepReadPause false at JSNodeHTTPServerSocketPrototype.cpp:314), lifting the body pause set at src/js/internal/http.ts:200-203, and the next read is parsed because readsBehindOwnFin is now true. Fix: pass keepReadPause true (or skip the resume when socket->flags.is_paused was set by a body pause) on the nothing-queued end() path, as halfCloseAfterDrain already does.

    Why this was flagged

    Trigger: a request body larger than the IncomingMessage highWaterMark; push() fails at src/js/internal/http.ts:190 and the socket is paused at :200-203. The handler then calls req.socket.end() with nothing queued. jsFunctionNodeHTTPServerSocketEnd at JSNodeHTTPServerSocketPrototype.cpp:314 calls halfClose(globalObject, false); cycleReads at JSNodeHTTPServerSocket.cpp:391 is true, so :401 calls us_socket_resume and clears flags.is_paused. On the base the same resume happened but HttpContext.h:342 dropped every read behind the FIN. On this branch deferShutdownUntilResponseDrains at :356 set HTTP_NODE_SHUTDOWN_AFTER_DRAIN first, so the next read (up to the 512 KB read buffer) is parsed and pushed into a stream whose consumer asked for backpressure; only then does :195-203 re-pause or stopParsing. The deferred-FIN path avoids this with keepReadPause true at :1116. Remedy: pass keepReadPause true from the nothing-queued end() path as well.

    Verification: The request body already filled the IncomingMessage buffer and the handler calls req.socket.end() with nothing queued. jsFunctionNodeHTTPServerSocketEnd (JSNodeHTTPServerSocketPrototype.cpp:314) calls halfClose(globalObject, false); cycleReads at JSNodeHTTPServerSocket.cpp:391 is true, so :400 us_socket_resume(socket) clears is_paused and one more read (≤ 524288) is parsed into the already-full IncomingMessage.

  • 🟡 packages/bun-uws/src/HttpContext.h — On Windows a connection that stopped parsing mid-read keeps the event loop alive until the peer closes; the socket also stays in a cork slot. stopParsing() is now called from JavaScript inside a read (src/js/internal/http.ts:197 and src/js/node/_http_server.ts:969); when the same read holds a later request head, the request lambda returns nullptr at packages/bun-uws/src/HttpContext.h:474, so returnedData is null and the us_socket_unref and uncork at HttpContext.h:810/844 are skipped. The ref taken at the top of onData is uv_ref on libuv. Fix: when HTTP_NODE_PARSING_STOPPED ends a parse mid-read, run the same unref and uncork the closed-socket branch at HttpContext.h:459-460 runs before returning nullptr.

    Why this was flagged

    Trigger: a client sends a request whose body is larger than req's high-water mark together with a pipelined request head in one read, the handler calls req.socket.end() with nothing queued, and nothing reads the body. onDataIncomingMessage at src/js/internal/http.ts:195-197 sees finSent and calls socket[kHandle].stopParsing(), setting HTTP_NODE_PARSING_STOPPED. The parser then reaches the next head in the same read and the request lambda returns nullptr at packages/bun-uws/src/HttpContext.h:474 without the unref/uncork the other early return does at HttpContext.h:459-461. onData's returnedData is therefore null (HttpContext.h:796) and the us_socket_unref at :810 is skipped. On libuv (Windows) us_socket_ref is uv_ref, so this socket keeps the loop alive until it closes, which happens only at the peer's FIN (loop.c:927). On the base this nullptr return was reached only through onSocketCloseStopParsing, a path the user triggers explicitly; the diff makes it a normal consequence of socket.end() with an unread body.

    Verification: packages/bun-uws/src/HttpContext.h:328 takes us_socket_ref(s) at the top of onData; the HTTP_NODE_PARSING_STOPPED check at lines 473-474 does a bare return nullptr; with no unref, so the us_socket_unref at line 810 is skipped. On Windows us_socket_ref/unref are uv_ref/uv_unref (packages/bun-usockets/src/socket.c:813-818, 858-863). The cork claim is wrong: line 902 uncorks on the nullptr path.

Comment thread src/js/node/_http_server.ts Resolved
Comment thread src/js/internal/http.ts Resolved
…er server.close()

A full buffer drops the rest of a body behind the FIN only when nothing
reads it. A reader gets backpressure, also after server.close().
…n flight and no other request

A request that arrives behind the FIN gets no response, so it is not
dispatched. A request body that nothing reads pauses the reads while the
FIN waits and is dropped behind it. requestTimeout ends a connection that
its peer does not end.
@robobun

robobun commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 9:05 PM PT - Oct 21st, 2026

@claude, your commit 967ab69 is building: #130459

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two points from the earlier round (a slower-than-peer reader losing its body at server.close(), and the new cases running serially) are addressed by the later commits. Beyond the inline findings, I also checked the new us_socket_unref in the HTTP_NODE_PARSING_STOPPED early return of the request lambda in packages/bun-uws/src/HttpContext.h: it is the only nullptr return from that lambda that leaves the socket open without an unref, parsing stops there so it runs at most once per onData, and it mirrors the isDrainingBeforeClose path, so the onData ref stays balanced. I also checked that the requestTimeout re-arm in stopReadsBehindOwnFin is reachable only behind this side's FIN (both call sites gate on us_socket_is_shut_down plus HTTP_NODE_SHUTDOWN_AFTER_DRAIN), so ordinary keep-alive connections do not pick up the new ERR_HTTP_REQUEST_TIMEOUT.

Extended reasoning...

Findings-present ruled-out note: the onData ref/unref balance on the new early return and the reachability of the timeout re-arm were traced through HttpContext.h, HttpResponseData.h and JSNodeHTTPServerSocket.cpp and found consistent; the three confirmed inline findings stand on their own.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread packages/bun-uws/src/HttpResponseData.h Resolved
Comment thread src/js/internal/http.ts Resolved
Comment thread src/ast/b.rs
Comment on lines +80 to +81
// No `impl Default` on purpose: only `is_single_line`/`close_brace_loc` have
// sensible defaults; `properties` does not, so every constructor must supply it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/ast/e.rs
Comment on lines +1186 to +1187
/// The tape rows that hold [`Self::properties`]. Code that keeps the tape alive longer than
/// this node can store the range and read it back with [`JsonTape::properties`].

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/ast/lib.rs
Comment on lines +642 to +643
/// Text on the line, avoiding the need to refetch the source code. It may be prefixed by the
/// preceding lines, which are printed too.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/ast/lib.rs
Comment on lines +1104 to +1105
/// Returns a source line as it is shown in a diagnostic: control characters other than tabs are
/// written as escape sequences such as `\x1b`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/ast/lib.rs
Comment on lines +1119 to +1120
/// Width in terminal columns of the first `units` UTF-16 code units of `line`, as `printable`
/// renders them. A tab counts as one column.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment on lines +436 to +437
/// RFC 5929 §4.1 `tls-server-end-point`: hashed with the digest of the signature algorithm,
/// SHA-256 in place of MD5 and SHA-1. `None` when the algorithm names no digest (Ed25519).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment on lines +1 to +16
//! `ArenaBox<'a, T>` — an owning pointer to one block of a [`MimallocArena`],
//! as wide as `Box<T>`: 8 B for a sized `T`, 16 B for a slice.
//!
//! `Box<T, &'a MimallocArena>` stores the allocator handle next to the pointer
//! (16 B / 24 B). The handle is only needed to allocate: `mi_free` finds the
//! heap of a block from its address, which `<&MimallocArena as
//! Allocator>::deallocate` and [`crate::transfer_arena`] already rely on. So an
//! `ArenaBox` is allocated through an arena that is passed in, and frees its
//! block without one. The lifetime keeps it from outliving the arena.
//!
//! Dropping it drops the value and returns the block to the heap at once, from
//! any thread. What is still allocated when the arena is dropped is bulk-freed
//! without `Drop`, like everything else in an arena.
//!
//! Not `Clone`, because a clone has to be allocated: see
//! [`ArenaBox::copy_from_slice_in`].

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment on lines +28 to +30
/// Frees a block of any [`MimallocArena`], and allocates nothing. Zero-sized.
///
/// Private, and not `Clone`: `Box<T, A>: Clone` would allocate through it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment on lines +97 to +98
/// If the iterator reports its exact length, one block of exactly that
/// size. Otherwise the block is resized at the end.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/bun_alloc/baby_vec.rs
Comment on lines +326 to +327
/// The elements in a block of their exact size. They are copied only if the allocator moves
/// the block to resize it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/bun_core/Global.rs
Comment on lines +790 to +791
/// Kills the process with `sig`'s default action. Exits with 128 + `sig` if the kernel discards
/// the signal (PID 1 in a pid namespace). Async-signal-safe.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/bun_core/Global.rs
Comment on lines +865 to +866
/// True if TLS should also trust the system's certificate authorities. `root_certs.cpp` reads this
/// when it builds the default certificate store.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/context.rs
Comment on lines +113 to +114
/// True if there's enough stack left to write one more nested expression, statement or type. If not, this sets
/// `ran_out_of_stack`, and the document must be discarded.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/context.rs
Comment on lines +183 to +184
/// Sets whether the content being written is inside a call to a function that doesn't take classes. Returns the
/// previous value, if there is a context.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/fields.rs
Comment on lines +1 to +5
//! ESTree node fields, as methods on the handles.
//!
//! Use `match e.kind()` to destructure a node. These methods are for checking one field of a node whose kind is known,
//! usually a parent: `parent.object() == Some(e)`. Each returns `None`, or an empty list, for a node without that
//! field.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/fields.rs
Comment on lines +17 to +18
/// `.argument` of a `UnaryExpression`, an `UpdateExpression`, an `AwaitExpression`, a
/// `YieldExpression` or a `SpreadElement`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/fields.rs
Comment on lines +20 to +22
/// `.expression` of a `TSAsExpression`, a `TSSatisfiesExpression`, a `TSTypeAssertion`, a
/// `TSNonNullExpression` or a `TSInstantiationExpression`, and the index of a
/// `ComputedMemberExpression`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/fields.rs
Comment on lines +24 to +25
/// `.typeAnnotation` of a `TSAsExpression`, a `TSSatisfiesExpression` or a `TSTypeAssertion`.
/// `None` for `as const`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/format.rs
Comment on lines +1 to +13
//! [`Format`] impls for the `bun_lint::ast` handles.
//!
//! `write!(f, expr)` runs the steps that are common to every node, and calls the `print/` function for this kind of
//! node:
//! 1. If the node is ignored (`// prettier-ignore`), its source text is copied instead of steps 4 to 6.
//! 2. `/** @type {T} */ (e)` keeps its parentheses.
//! 3. The leading comments of the node.
//! 4. `(`, if the node needs parentheses in this position: `parentheses/`.
//! 5. The node.
//! 6. `)`
//! 7. The trailing comments of the node.
//!
//! Most nodes have no comment anywhere near them. While [`Formatter::is_quiet`] is true, only steps 4 to 6 run.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/format.rs
Comment on lines +26 to +27
/// True while writing a node that has no comment inside its span, and no node inside it that owns a comment from
/// elsewhere.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/format.rs
Comment on lines +54 to +55
// The nodes that contain the cursor region, and the nodes right before and after it, aren't quiet, so this
// still gets called for them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/format.rs
Comment on lines +77 to +78
/// Returns the span from Prettier's `locStart` to its `locEnd`. It includes the decorators before an `export` and
/// excludes the `;` of a JavaScript statement.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/format.rs
Comment on lines +98 to +99
// These end where their last body ends. If that body is an empty statement, its `;` is the statement itself and
// stays in the span.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/format.rs
Comment on lines +119 to +120
/// Writes `node` with the comments it owns: Prettier's `printComments`. `write` writes the node itself. If the node is
/// ignored, its source text is copied instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/format.rs
Comment on lines +152 to +169
/// Writes an ignored node with the comments it owns. `write_source_text` copies the node, and marks the comments in the
/// copied text as printed.
///
/// A comment that the node owns can be inside the node. It's copied with the node, so it isn't printed here:
///
/// ```js
/// a
/// // prettier-ignore
/// .b;
/// ```
///
/// A dangling comment can be outside the node: Prettier moves this one into the empty body, copies the body, and then
/// throws because the comment wasn't printed. Here it's printed before the body.
///
/// ```js
/// class A // prettier-ignore
/// {}
/// ```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/format.rs
Comment on lines +196 to +197
/// Writes a node whose own printer prints its leading and trailing comments (Prettier's `willPrintOwnComments`). This
/// only handles `prettier-ignore`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/format.rs
Comment on lines +226 to +228
/// [`format_name`] for a name that Prettier prints like any other node, so a `prettier-ignore` comment applies to it:
/// `import /* prettier-ignore */ * as a from /* prettier-ignore */ 'b'`, `a./* prettier-ignore */ \u{62}`. Don't use
/// it for a property key.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/format.rs
Comment on lines +256 to +257
/// Prettier's `printIgnored`: copies the source text of `node`, adding the parentheses and the `;` it needs in its
/// position. This is the only place that writes an ignored node.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/format.rs
Comment on lines +297 to +298
/// A name that's a node in ESTree but not here, with the comments around it: the `b` of `a.b`, the name of a function,
/// a label.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/format.rs
Comment on lines +325 to +326
/// Options for writing a function or arrow function, which depend on its position. See `print/function.rs` and
/// `print/arrow_function_expression.rs`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/format.rs
Comment on lines +354 to +355
/// `ChainExpression.expression`: `expr` is a whole optional chain, and this formats it as the member access or call
/// inside the `ChainExpression`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/format.rs
Comment on lines +399 to +402
// typescript-estree has a `ChainExpression` around an optional chain, which can't own comments. The comments
// belong to the member access or the call in it, so they're printed inside its parentheses. Babel has no such
// node, so there they're printed outside. oxfmt prints them outside too, because its `ChainExpression` prints
// them: `(/* comment */ a?.b).c` is `/* comment */ (a?.b).c`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/format.rs
Comment on lines +411 to +412
// A JSX element prints its comments together with its parentheses. Prettier copies an ignored element without
// them: `((<a />) /* prettier-ignore */, b)`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/format/js/format.rs
Comment on lines +447 to +448
/// `(/** @type {T} */ () => {})();`: for oxfmt a comment that looks like a type cast stays directly before its `(`,
/// which here is the `(` of the parameters. Prettier prints it before the parentheses around the function.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

@dylan-conway
dylan-conway changed the base branch from robobun/a085ff54/node-http-socket-end-fin-after-drain to main October 21, 2026 23:45
@dylan-conway
dylan-conway marked this pull request as ready for review October 21, 2026 23:52
@coderabbitai

coderabbitai Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 14 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: e87c3e8b-87eb-4074-952d-e7cc81d2f521
📥 Commits

Reviewing files that changed from the base of the PR and between 0793250 and 967ab69.

📒 Files selected for processing: 7
  • packages/bun-uws/src/HttpContext.h
  • packages/bun-uws/src/HttpParser.h
  • packages/bun-uws/src/HttpResponseData.h
  • src/js/internal/http.ts
  • src/jsc/bindings/node/JSNodeHTTPServerSocket.cpp
  • src/jsc/bindings/node/JSNodeHTTPServerSocketPrototype.cpp
  • test/js/node/http/node-http-server-socket-end-drain.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-reviewed the reworked version of this PR (the HTTP_NODE_HALF_CLOSED / readsBodyBehindOwnFin design) and found no bugs; since it changes which shut-down early-outs in uWS's onData let bytes through, a maintainer's look is still worthwhile.

What was reviewed:

  • Traced HTTP_NODE_HALF_CLOSED from its only setter (the no-wait branch of deferShutdownUntilResponseDrains) through the three bypassed checks in HttpContext.h::onData; the fin clause on the per-chunk check still stops the parse at the last chunk, so a pipelined request behind the body is never dispatched, and HTTP_NODE_PARSING_STOPPED / isDrainingBeforeClose early-outs still drop later reads.
  • Checked the us_socket_ref balance: the top-level early-out still unrefs, and the new bypass only reroutes to the existing returnedData path; Bun.serve (IsNodeHttp=false) never sets the flag, so its behavior is unchanged.
  • Checked hasIncompleteRequestBody against ChunkedEncoding.h: STATE_IS_TRAILERS_DONE is only reached after the complete trailer section, and Content-Length bodies already hit 0 before the handler runs, so a genuinely cut-short body still reports HPE_INVALID_EOF_STATE.
  • Checked destroySoon(): the fin return at the per-chunk check skips the in-callback closeDestroyedNodeHttpSocket, but the post-parse HTTP_NODE_CLOSE_AFTER_MESSAGE gate still runs it.
Extended reasoning...

The diff adds a connection-scoped state bit in packages/bun-uws/src/HttpResponseData.h, a predicate and three bypasses in HttpContext.h::onData, a one-clause change to HttpParser::hasIncompleteRequestBody, a read-only finSent getter on JSNodeHTTPServerSocket, and a stopParsing/pause decision in src/js/internal/http.ts, plus a describe.each test suite over http and https. It touches the node:http request-parsing path (peer-controlled bytes behind a server-sent FIN) but no auth, crypto or injection surface. It was not approved because the change alters shutdown gating in native HTTP parsing code, the PR conversation could not be read in full this run, and a maintainer has open inline comments posted after the latest commit whose text is not visible here.

Copy link
Copy Markdown
Member

buildkite/bun is red on 967ab69 (#130459), and the cause is not in this PR.

  • The only jobs that did not pass are the two darwin any x64 - test-bun jobs. They expired at 01:05 UTC without a start time: no agent with os=darwin arch=x64 queue=test-darwin took them, so no test ran.
  • The other 179 jobs passed. That includes darwin x64 - build-bun and every other test-bun lane: darwin aarch64, Windows x64 and aarch64, Alpine, Debian (also ASAN) and Ubuntu.
  • The build has no error annotation, so no test failure was recorded.

BuildKite has already queued a retry of both jobs. Nothing in the branch can change this, so there is no fix to push.


Generated by Claude Code

@dylan-conway
dylan-conway merged commit 9bd124c into main Oct 22, 2026
9 of 11 checks passed
@dylan-conway
dylan-conway deleted the robobun/c5aec4a7/upgrade-body-half-open branch October 22, 2026 02:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants