Skip to content

node:http: emit 'data' on the server 'connection' socket with the raw request bytes - #35661

Closed
robobun wants to merge 8 commits into
mainfrom
farm/59aeeffb/http-connection-socket-data
Closed

robobun wants to merge 8 commits into
mainfrom
farm/59aeeffb/http-connection-socket-data

Conversation

@robobun

@robobun robobun commented Jul 25, 2026 •

Copy link
Copy Markdown
Collaborator

A node:http server's 'connection' event hands over a socket whose 'data' event never fires for normal requests. In Node that socket is the net.Socket the parser reads from, so every 'data' listener (the parser's included) receives the raw HTTP request bytes.

const http = require('http');
const server = http.createServer((req, res) => res.end('ok'));
server.on('connection', socket => {
  socket.on('data', d => console.log('data:', d.length));   // node: fires with raw request bytes
});                                                         // bun: never fires
server.listen(0, () => fetch(`http://127.0.0.1:${server.address().port}/`));

Cause

Bun's node:http server parses HTTP natively in uWS: HttpContext::onData consumes the TCP payload and dispatches the parsed request straight to onNodeHTTPRequest. The JS NodeHTTPServerSocket wrapper is a Duplex whose readable side is only fed in CONNECT/Upgrade tunnel mode (via handle.ondata), so for an ordinary request nothing ever pushes into it and 'data'/'readable' never fire.

Fix

Add a raw-bytes tap in HttpContext::onData that forwards each TCP payload, before HTTP parsing (and before the parser-stopped gate, so a custom 'clientError' handler that keeps the socket open keeps seeing bytes), to a new onrawdata callback slot on JSNodeHTTPServerSocket. The slot is only populated when the JS socket actually has a 'data'/'readable' listener (checked right after emit('connection') and on each _read()); when it is null the native tap reduces to a single null-check, so the common path (no user listener) pays no Buffer allocation or JS call. The JS side pushes the chunk into the Duplex, so 'data'/'readable' fire with the raw request bytes, and socket.bytesRead advances.

Once armed the tap stays armed; the native !isConnectRequest gate (set synchronously by upgradeToTunnel()) stops it for tunnel payloads, and #resumeSocket() skips its parsed-body socket push when the tap owns the readable side.

Why this is the right fix

In Node, connectionListenerInternal attaches socketOnData as a 'data' listener on the connection's net.Socket; the parser is fed from that event, so a second user listener sees the same bytes. Replicating that exactly would mean round-tripping every HTTP byte through JS, defeating the native parser. Tapping the raw payload only when a user actually subscribes preserves the native fast path while matching the observable contract: a 'data' listener on the 'connection' socket receives the raw request bytes (request line, headers, body framing and all), same as Node.

Verification

New test/js/node/http/node-http-connection-socket-data.test.ts (all six fail on main, pass with this change, and the first five match Node v26 exactly):

  • 'connection' socket 'data' carries the full request line + headers and updates bytesRead
  • two keep-alive body-bearing requests (Content-Length and chunked) both reach the listener with framing intact
  • 'readable' + read() yields the same bytes
  • https.Server 'secureConnection' socket delivers the decrypted request bytes (Node's https 'connection' hands over the raw net.Socket, whose 'data' never fires there; the TLSSocket is on 'secureConnection')
  • a req.socket.on('data') added inside the request handler sees subsequent raw bytes
  • a CONNECT request's line + head bytes reach the listener once (no double-delivery)

No regressions in node-http.test.ts, the CONNECT/Upgrade suites, node-http-req-socket-pause, node-http-backpressure, node-http-transfer-encoding, node-http-server-timeouts, or Node's upstream test-http-connect* / test-http-upgrade* / test-http-server-unconsume* tests.

Known differences from Node

  • The tap is a passive observer of bytes the native parser is consuming anyway, so socket.pause() on the 'connection' socket does not throttle HTTP parsing (Node pauses the fd, which stops its parser too). The Readable buffer grows while paused, bounded by what the connection already holds.
  • The tap does not push(null) on peer FIN for a non-tunnel connection; 'close' fires as before.

Fixes #11924.

… request bytes

Node's http.Server feeds its parser from the connection socket's 'data'
event, so a user listener on that socket sees the raw HTTP request bytes
alongside the parser. In Bun the native uWS parser consumes the TCP payload
internally and the JS socket wrapper never received those bytes, so
socket.on('data') inside a 'connection' handler never fired.

Add a raw-bytes tap in HttpContext::onData that forwards each TCP payload
to a new onrawdata slot on JSNodeHTTPServerSocket before parsing. The slot
is only populated when the JS socket actually has a 'data'/'readable'
listener (checked after emit('connection') and on each _read()); when it is
null the native tap is a single null check. Tunnel mode (CONNECT/Upgrade)
keeps its existing ondata path and clears onrawdata on entry so bytes are
not delivered twice.

Fixes #11924.
@coderabbitai

coderabbitai Bot commented Jul 25, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 410bf5d9-ec4f-4f11-9c58-b25dbe3028b7

📥 Commits

Reviewing files that changed from the base of the PR and between 5c4cda7 and 98320ea.

📒 Files selected for processing (8)
  • packages/bun-uws/src/App.h
  • packages/bun-uws/src/HttpContext.h
  • packages/bun-uws/src/HttpContextData.h
  • src/js/node/_http_server.ts
  • src/jsc/bindings/NodeHTTP.cpp
  • src/jsc/bindings/node/JSNodeHTTPServerSocket.cpp
  • src/jsc/bindings/node/JSNodeHTTPServerSocket.h
  • test/js/node/http/node-http-connection-socket-data.test.ts

Walkthrough

Adds a raw socket data path for Node HTTP compatibility. Native HTTP payloads are forwarded before parsing, bridged into JavaScript, exposed through socket data/readable streams, coordinated with streaming mode, and covered by HTTP/HTTPS tests.

Changes

Raw Node HTTP socket data

Layer / File(s) Summary
Native raw-data tap
packages/bun-uws/src/App.h, packages/bun-uws/src/HttpContext*.h
Adds the raw socket callback and invokes it before HTTP parsing, stopping processing if the callback closes the socket.
JavaScript callback bridge
src/jsc/bindings/NodeHTTP.cpp, src/jsc/bindings/node/JSNodeHTTPServerSocket.*, src/jsc/bindings/node/JSNodeHTTPServerSocketPrototype.cpp
Adds the onrawdata accessor, native forwarding, Uint8Array callback delivery, exception handling, and GC marking.
Socket stream integration and validation
src/js/node/_http_server.ts, test/js/node/http/node-http-connection-socket-data.test.ts
Enables raw delivery for data/readable listeners, coordinates backpressure and streaming mode, and tests HTTP, HTTPS, keep-alive, readable, and late-listener cases.

Possibly related PRs

Suggested reviewers: cirospaciari

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main change: emitting raw request bytes on server connection sockets.
Description check ✅ Passed The description covers what changed, why, and how it was verified, which satisfies the template despite using different section headings.

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

Comment thread src/js/node/_http_server.ts Outdated
Comment thread src/js/node/_http_server.ts Outdated
Comment thread src/js/node/_http_server.ts Outdated
Comment thread src/js/node/_http_server.ts Outdated
Comment thread src/jsc/bindings/node/JSNodeHTTPServerSocket.cpp Outdated
@robobun

robobun commented Jul 25, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 5:08 PM PT - Jul 25th, 2026

@robobun, your commit 98320ea is building: #81201

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. req.socket.bytesRead returns 0 when using node:http server #28709 - PR updates bytesRead on the connection socket when forwarding raw request bytes, directly fixing the reported req.socket.bytesRead always returning 0

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #28709

🤖 Generated with Claude Code

@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.

Additional findings (outside current diff — PR may have been updated during review):

  • 🔴 src/js/node/_http_server.ts:1918-1922 — #resumeSocket() still does this.push(resumed) at line 1911 with the parsed request body drained from response.resume(), while the new raw tap (#onRawData) also pushes the raw TCP payload containing those same body bytes into the same Duplex — so under IncomingMessage backpressure the connection-socket 'data' listener receives the body twice (once inside the raw HTTP framing, once as a bare parsed chunk). Gate line 1911 on !handle.onrawdata (or drop it — the raw tap now owns the socket's readable side).

    Extended reasoning...

    What the bug is

    This PR adds #onRawData which pushes every raw TCP payload into the NodeHTTPServerSocket Duplex so a 'data' listener on the 'connection' socket sees the raw request bytes. But the pre-existing #resumeSocket() still contains this.push(resumed) at line 1911, which pushes the parsed request body (drained from response.resume()'s buffered_request_body_data_during_pause) into the same Duplex. _read() at lines 1916–1922 now calls both maybeEnableRawSocketData(this) (arming the raw feed) and this.#resumeSocket() (the parsed-body feed), and resume() at line 2078 also calls #resumeSocket(). Nothing gates line 1911 on whether handle.onrawdata is set.

    Code path that triggers it

    1. server.on('connection', s => s.on('data', ...)) — after emit('connection'), maybeEnableRawSocketData(socket) sets handle.onrawdata = #onRawData.bind(this).
    2. A POST request arrives whose body is not immediately consumed. onDataIncomingMessage (internal/http.ts:261–266) calls req.push(chunk); when the IncomingMessage's 16 KB highWaterMark fills, push() returns false → socket.pause().
    3. NodeHTTPServerSocket.pause() (line 2067) calls response.pause() → do_pause() re-registers the native inStream to on_buffer_paused_shim, which appends every subsequent body chunk to buffered_request_body_data_during_pause. This buffer becomes non-empty when (a) the request is chunked and more chunks in the same TCP packet follow the one that triggered the pause (the parser keeps running through the current onData buffer), or (b) on Windows, where do_pause skips pause_socket() so subsequent packets keep arriving.
    4. Meanwhile the raw tap in HttpContext::onData fires before parsing: for that same TCP packet, onSocketRawData → JSNodeHTTPServerSocket::onRawData has already posted a task that will this.push(rawPacketBytes) into the Duplex. On Windows, subsequent packets are also raw-tapped.
    5. When the IncomingMessage drains, IncomingMessage._read → socket.resume() (line 2077) → #resumeSocket() → response.resume() returns the buffered parsed body as a Buffer → line 1911 does this.push(resumed).

    Step-by-step proof

    Take a chunked POST whose single TCP packet contains [headers][chunk-1: 20 KB][chunk-2: 4 KB][0\r\n\r\n], with a 'connection'-socket 'data' listener attached and a request handler that does not touch req:

    • HttpContext::onData receives the whole packet. The raw tap posts a task with a Buffer of the entire raw packet (headers + 14000\r\n<20KB>\r\n1000\r\n<4KB>\r\n0\r\n\r\n).
    • The parser dispatches the request, then delivers chunk-1 via inStream → onDataIncomingMessage → req.push(20KB) returns false (HWM = 16 KB) → socket.pause() → response.pause() swaps inStream to on_buffer_paused_shim.
    • Still inside the same onData, the parser delivers chunk-2 via the swapped inStream → on_buffer_paused_shim appends the 4 KB to buffered_request_body_data_during_pause.
    • The posted raw-tap task runs: #onRawData → this.push(<raw packet>) — the socket 'data' listener receives the full raw packet, including the 4 KB of chunk-2.
    • Later, IncomingMessage._read → socket.resume() → #resumeSocket() → response.resume() returns Buffer<4KB> → req.push(4KB) and this.push(4KB) (line 1911). The socket 'data' listener now receives an extra 4 KB chunk — the parsed body of chunk-2, with no HTTP framing.

    The listener's byte stream is <raw packet><4KB body> instead of <raw packet>. On Windows this applies to every body packet after the pause, not just the tail of one.

    Why existing code doesn't prevent it

    maybeEnableRawSocketData gates the tap on !socket[kStreamingEnabled], and [kEnableStreaming](true) (tunnel mode) clears handle.onrawdata. But nothing in #resumeSocket() checks handle.onrawdata before line 1911. Before this PR, this.push(resumed) was the only thing feeding the socket's readable side (and it almost never fired, which is why #11924 exists). Now there are two feeds into the same Duplex, and they overlap on exactly the bytes the pause buffer captured. None of the new tests exercise this: test 2 does req.resume() so the IncomingMessage never backpressures.

    Impact

    The PR's stated contract is that a 'data' listener on the 'connection' socket "receives the raw request bytes" — i.e. the wire bytes. When triggered, the stream contains bytes that were never on the wire in that position (a bare body slice with no chunk framing, appearing after the raw packet that already contained it). Any consumer that concatenates the socket's 'data' chunks and re-parses them (proxies, request loggers, HTTP capture tooling — the primary use case for this feature) will see a corrupted stream. The trigger — a POST body large enough to fill IncomingMessage's 16 KB HWM before the request handler reads it — is realistic, and on Windows it affects the entire body after the first pause.

    Fix

    Gate the socket-side push on the raw tap not being armed:

    if (req) {
      req.push(resumed);
    }
    if (!handle.onrawdata) this.push(resumed);

    Or simply drop this.push(resumed) — with the raw tap in place it is redundant (Node's socket carries raw bytes, not parsed body, so this line was never Node-correct anyway; the raw tap now covers what it was approximating). The req.push(resumed) on line 1909 must stay — that feeds the IncomingMessage, not the socket.

Comment thread packages/bun-uws/src/HttpContext.h Outdated
Comment thread test/js/node/http/node-http-connection-socket-data.test.ts Outdated
Comment thread src/js/node/_http_server.ts
Comment thread src/js/node/_http_server.ts
…et push, tighten tests

- Move the raw-bytes tap above the HTTP_NODE_PARSING_STOPPED early-return so a
  user 'data' listener keeps seeing bytes after a custom 'clientError' handler
  leaves the connection open (Node removes only its own 'data' listener there).
- Gate #resumeSocket's socket-side push on !handle.onrawdata: the raw tap owns
  the socket readable; pushing the parsed body and push(null) there would
  double-deliver body bytes and end the readable mid-keep-alive.
- Count tunnel-mode bytes in socket.bytesRead (#onData), completing the pair.
- Wrap tests 2, 4, 5 in try/finally so the server and client are released on
  assertion failure.
- Trim multi-line comments flagged by the comment-cop check.
Comment thread src/js/node/_http_server.ts Outdated
Comment thread src/js/node/_http_server.ts Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/js/node/http/node-http-connection-socket-data.test.ts (1)

1-186: 🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy lift

Missing test coverage for backpressure and CONNECT/Upgrade tap-disable interaction.

Good coverage for the happy paths, but no test exercises a slow consumer causing socket.push() backpressure (which is where a genuine data-loss bug exists — see the _http_server.ts review) or verifies the raw tap doesn't double-deliver / lose bytes across a CONNECT/Upgrade tunnel switch. Consider adding cases for both, per the "every behavioral change needs a test" guideline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/js/node/http/node-http-connection-socket-data.test.ts` around lines 1 -
186, Extend the HTTP socket data tests with cases covering a slow consumer that
triggers socket.push() backpressure, asserting all raw request bytes remain
observable without duplication or loss. Also add a CONNECT or Upgrade test that
verifies the raw socket tap is disabled exactly once during tunnel transition
and preserves bytes sent before and after the switch. Anchor the additions near
the existing http.Server connection and req.socket data tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/node/_http_server.ts`:
- Around line 1640-1654: Update the raw-data backpressure handling in `#onRawData`
and its kEnableRawData re-arm path so a false result from this.push(chunk)
pauses the underlying socket read rather than only clearing handle.onrawdata,
and resume it when demand returns. Coordinate the tap lifecycle with the
existing socket pause/resume mechanism and preserve bytes until _read()
re-enables delivery, including the corresponding path around the later raw-data
handling block.

---

Outside diff comments:
In `@test/js/node/http/node-http-connection-socket-data.test.ts`:
- Around line 1-186: Extend the HTTP socket data tests with cases covering a
slow consumer that triggers socket.push() backpressure, asserting all raw
request bytes remain observable without duplication or loss. Also add a CONNECT
or Upgrade test that verifies the raw socket tap is disabled exactly once during
tunnel transition and preserves bytes sent before and after the switch. Anchor
the additions near the existing http.Server connection and req.socket data
tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 954b8871-9683-4f20-9926-1e44acfd73f8

📥 Commits

Reviewing files that changed from the base of the PR and between df6c7ee and 5c4cda7.

📒 Files selected for processing (9)
  • packages/bun-uws/src/App.h
  • packages/bun-uws/src/HttpContext.h
  • packages/bun-uws/src/HttpContextData.h
  • src/js/node/_http_server.ts
  • src/jsc/bindings/NodeHTTP.cpp
  • src/jsc/bindings/node/JSNodeHTTPServerSocket.cpp
  • src/jsc/bindings/node/JSNodeHTTPServerSocket.h
  • src/jsc/bindings/node/JSNodeHTTPServerSocketPrototype.cpp
  • test/js/node/http/node-http-connection-socket-data.test.ts

Comment thread src/js/node/_http_server.ts
Comment thread src/js/node/_http_server.ts Outdated
Comment thread src/js/node/_http_server.ts Outdated
Comment thread src/js/node/_http_server.ts Outdated
robobun added 2 commits July 25, 2026 12:33
…l entry

The native !isConnectRequest gate (set synchronously by upgradeToTunnel())
already stops the raw tap for tunnel payloads, so clearing handle.onrawdata in
[kEnableStreaming](true) only dropped the already-posted CONNECT/Upgrade
request-head chunk. Disarming on push()==false in #onRawData dropped an
interior slice of the wire stream (the parser keeps reading the socket
regardless) and left handle.onrawdata transiently null so the #resumeSocket
gate opened and pushed the parsed body a second time. With the tap never
disarmed, handle.onrawdata is a stable ownership predicate and the Readable
buffer is bounded by what the connection already holds.

Adds a CONNECT test that asserts the request line reaches the 'data' listener
exactly once and tunnel bytes follow without duplication.
…en CONNECT assertions

Node's https.Server emits the raw net.Socket on 'connection' (its 'data' never
fires there) and the TLSSocket on 'secureConnection'; the decrypted-bytes test
now listens on 'secureConnection' so it actually matches Node. The CONNECT test
drops the tunnel-byte assertion (Node sets flowing=null before 'connect' so a
'connection'-time listener stops there) and polls for EXTRA instead.
Comment thread src/js/node/_http_server.ts
Comment thread packages/bun-uws/src/HttpContext.h
Comment thread src/js/node/_http_server.ts
…after secureConnection

When an Upgrade-with-body's body-fin and first tunnel bytes share one TCP read,
the raw tap delivers the whole read and the tunnel path would deliver the tail
again via onSocketData. onSocketRawData now returns whether the JS callback was
armed; the dataHandler lambda captures that and skips its onSocketData call
when the raw tap already covered this read. CONNECT and body-less Upgrade reach
the tunnel path with isConnectRequest already true at onData entry, so the raw
tap was skipped there and onSocketData remains the sole producer.

Also move maybeEnableRawSocketData() after emit('secureConnection') so a
listener attached there arms the tap synchronously like the 'connection' path.

@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 latest push looks good — rawTapCoveredThisRead in the data-handler lambda cleanly resolves the Upgrade-with-body tunnel-tail double-delivery, and maybeEnableRawSocketData now runs after emit('secureConnection') so both emit sites arm synchronously. No new issues found; the remaining 🟡 on HttpContext.h:310 is the pre-existing split-CONNECT parser flag write and is out of scope here.

That said, this threads new state through HttpContext::onData, adds a GC-visited callback slot with postTask delivery, and changes who owns the socket Duplex's readable side in #resumeSocket(), with two documented intentional divergences from Node. Worth a human pass on the design tradeoffs before landing.

What was reviewed:

  • rawTapCoveredThisRead capture-by-value into the data-handler lambda: verified it correctly gates onSocketData for the straddling packet while leaving subsequent tunnel reads (and the unarmed-tap case, where onRawData returns false) on the existing #onData path — bytesRead is incremented exactly once per byte.
  • onRawData's gcProtect/postTask/gcUnprotect matches the sibling onData pattern; functionToCallOnRawData is a WriteBarrier visited in visitChildrenImpl.
  • #resumeSocket()'s !handle.onrawdata gate is now stable (the tap never disarms), so parsed-body/EOF pushes are correctly skipped only when the raw tap owns the readable side.
Extended reasoning...

Overview

This PR makes a node:http server's 'connection' socket emit 'data' with the raw request bytes, matching Node's observable contract (Node feeds its parser from that same event). It adds an onSocketRawData tap in HttpContext::onData (before parsing and before the parser-stopped gate), a new onrawdata callback slot on JSNodeHTTPServerSocket (WriteBarrier + getter/setter + visitChildren), and JS-side wiring in _http_server.ts that arms the tap only when a user actually listens for 'data'/'readable'. The tap is a passive observer; the native parser keeps consuming regardless. Seven tests cover HTTP, HTTPS via 'secureConnection', keep-alive with bodies, 'readable'+read(), late-attached listeners, CONNECT head-once, and Upgrade-with-body tunnel-tail dedup.

Files: packages/bun-uws/src/{App.h,HttpContext.h,HttpContextData.h}, src/jsc/bindings/NodeHTTP.cpp, src/jsc/bindings/node/JSNodeHTTPServerSocket{.h,.cpp,Prototype.cpp}, src/js/node/_http_server.ts, plus a new test file.

Security risks

None identified. The tap is opt-in (only reachable when a user attaches a 'data'/'readable' listener on the connection socket) and copies bytes the parser is already consuming — it exposes no new data, adds no parsing of untrusted input, and touches no auth/TLS decisions. The one memory-growth vector (Readable buffer accumulating while the consumer is paused) is bounded by what the connection already holds and is called out in the PR body.

Level of scrutiny

High. HttpContext::onData is the per-read hot path for every node:http server connection, and the change captures a stack bool into a lambda that the parser calls back mid-parse. The C++ side adds a GC-visited slot with a gcProtect→postTask→gcUnprotect handoff (mirroring the existing onData sibling, which reduces risk). On the JS side, #resumeSocket() now skips its socket-readable pushes when the raw tap owns the readable — a real ownership change whose correctness depends on the tap never transiently disarming (which was the subject of an earlier 🔴 and is now fixed: #onRawData always pushes, and only [kEnableRawData] writes handle.onrawdata). The PR also documents two intentional divergences from Node (pause() doesn't throttle parsing; no push(null) on non-tunnel FIN) that are architectural rather than incidental.

Other factors

The PR has iterated through five fix commits in response to earlier 🔴 findings (dual-producer in #resumeSocket, backpressure disarm dropping bytes, CONNECT/Upgrade head loss via cleared callback, and the tunnel-tail double-count). Each is now addressed and covered by a test in the new file. The one open 🟡 (isConnectRequest written prematurely on split-CONNECT short-read) is a pre-existing HttpParser.h defect that already breaks split-CONNECT dispatch independently of this PR; fixing it here would be scope creep into a file the PR doesn't touch. Given the number of subtle interactions surfaced during review and the documented Node-divergence tradeoffs, a human should sign off on the design before merge.

@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

CI on 98320ea (build 81201) is red only on lanes unrelated to this diff:

  • freebsd x64 build-bun step failure (pre-existing)
  • complex-workspace.test.ts install flake on debian x64-asan (same as the prior build on main)
  • webview-chrome.test.ts, fetch-leak.test.ts, 20144.test.ts, no-orphans.test.ts: known flakes on darwin
  • test-http-server-connections-checking-leak.js on debian aarch64: passes locally with this build; the test creates servers without listening, so none of this PR's code paths (which activate only on an accepted connection with a 'data' listener) run

node-http-connection-socket-data.test.ts and the rest of the http suite are green on every lane that ran tests. The diff is ready for a maintainer to merge.

@github-actions

Copy link
Copy Markdown
Contributor

Closing this PR because it has been inactive for more than 90 days.

@github-actions github-actions Bot closed this Oct 24, 2026
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.

Unable to read raw data from http/https requests, using either "node:http" or "node:https"

1 participant