Deduplicate http client and websocket init internals#32020
Conversation
WalkthroughThis PR consolidates HTTP/2 header field validation, refactors HTTP client completion and redirect handling, simplifies WebSocket initialization, extracts socket-stub infrastructure for reuse, and refactors HTTP/2 frame-parser stream ID handling and header encoding logic. ChangesHTTP/2 Validation Extraction and Client/Parser Consolidation
Socket Stub Infrastructure Extraction and Application
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
|
Updated 3:05 AM PT - Jun 10th, 2026
❌ @robobun, your commit 601934f has some failures in 🧪 To try this PR locally: bunx bun-pr 32020That installs a local version of the PR into your bun-32020 --bun |
|
@robobun adopt |
|
Adopted. Audited the diff arm-by-arm against main: no behavior changes found. Added tests pinning the consolidated behavior (node-http2-header-validation.test.ts, node-http-server-socket-surface.test.ts); rationale in the PR description. Final CI on build 61716: 282 jobs passed with zero test failures across every lane that ran (all Linux, Windows, and darwin x64); the only red is 4 darwin-aarch64 test lanes that expired waiting for macOS agents and never ran. Ready for maintainer review. |
There was a problem hiding this comment.
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)
src/http/lib.rs (1)
3671-3680:⚠️ Potential issue | 🟠 Major | ⚡ Quick winKeep-alive reuse still misclassifies in-flight stream/sendfile uploads as drained.
request_side_drainedonly checks the unsent slice forHTTPRequestBody::Bytes(_); every other body kind returnstrue. That still lets an HTTP/1.1 socket be pooled after an early response while aStreamorSendfileupload is mid-flight, so the next request can reuse a connection whose previous request body is still being written/read.Suggested fix
- let request_side_drained = match &this.state.original_request_body { - HTTPRequestBody::Bytes(_) => this.state.request_body.is_empty(), - _ => true, - }; + let request_side_drained = match &this.state.original_request_body { + HTTPRequestBody::Bytes(_) => this.state.request_body.is_empty(), + HTTPRequestBody::Stream(_) | HTTPRequestBody::Sendfile(_) => { + this.state.request_stage == RequestStage::Done + } + };As per coding guidelines, "Fix the whole class in the same PR."
🤖 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 `@src/http/lib.rs` around lines 3671 - 3680, The pooling check incorrectly treats non-Bytes bodies as drained; change the request_side_drained logic so it returns true only when the original_request_body is a fully-sent Bytes (and request_body.is_empty()) or when it is explicitly an Empty/no-body variant, and return false for streaming/Sendfile variants so in-flight uploads block pooling; update the match on this.state.original_request_body (and any HTTPRequestBody variants like Stream, Sendfile, AsyncStream, etc.) to reflect that behavior so is_keep_alive_possible() && !socket.is_closed_or_has_error() && tunnel_poolable only proceeds when request_side_drained is truly drained.Source: Coding guidelines
🤖 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/internal/http.ts`:
- Around line 522-543: The setters (remoteAddress, remotePort, remoteFamily)
assume this.address() returns an object and crash when it returns
null/undefined; fix each setter to call const addr = this.address(); if (!addr)
create and assign a new backing object (e.g. this._address = {}) then use that
addr to set the property, so you initialize the address object on demand instead
of blindly writing into a possibly null value; apply this pattern in the
remoteAddress, remotePort, and remoteFamily setters.
---
Outside diff comments:
In `@src/http/lib.rs`:
- Around line 3671-3680: The pooling check incorrectly treats non-Bytes bodies
as drained; change the request_side_drained logic so it returns true only when
the original_request_body is a fully-sent Bytes (and request_body.is_empty()) or
when it is explicitly an Empty/no-body variant, and return false for
streaming/Sendfile variants so in-flight uploads block pooling; update the match
on this.state.original_request_body (and any HTTPRequestBody variants like
Stream, Sendfile, AsyncStream, etc.) to reflect that behavior so
is_keep_alive_possible() && !socket.is_closed_or_has_error() && tunnel_poolable
only proceeds when request_side_drained is truly drained.
🪄 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: 2bd442c1-2f2d-421c-a6c3-78c9e8436830
📒 Files selected for processing (8)
src/http/h2_client/dispatch.rssrc/http/lib.rssrc/http_jsc/websocket_client.rssrc/http_types/h2.rssrc/js/internal/http.tssrc/js/internal/http/FakeSocket.tssrc/js/node/_http_server.tssrc/runtime/api/bun/h2_frame_parser.rs
|
On the request_side_drained finding (outside the diff): that match and its comment exist verbatim on main (src/http/lib.rs, "Stream/Sendfile are left as-is, they do not track an unsent slice here"); this PR only renames self to this while moving the block into the closure. Changing the pooling predicate for Stream/Sendfile bodies would be a behavior change, which this PR deliberately avoids, and the proposed request_stage gate is unvalidated (the comment above it explains why Bytes cannot use request_stage: a fully sent small request parks at .body). If that predicate needs tightening it should be its own change with its own test. |
What this does
Extracts the shared WebSocket client init tail (
finish_init, taking an owned buffer — public signatures unchanged and safe), dedupes h2 client dispatch and header-validation helpers (shared viasrc/http_types/h2.rs, used byh2_frame_parser), and consolidates the JSFakeSocket/server-socket stub descriptors insrc/js/internal/http. Net −700 lines.Split from #31912 (whole-repo simplification pass; closing that PR in favor of module-scoped splits). This PR only moves and removes code — zero intended behavior change. Verified there by a per-file behavioral-equivalence audit and full CI; verified here by a standalone full-workspace compile check.
Tests
Adds coverage pinning the consolidated behavior:
test/js/node/http2/node-http2-header-validation.test.ts: covers both arms of the shared header-encoding closure (control characters in single and array header values, exactERR_HTTP2_INVALID_HEADER_VALUE/ERR_INVALID_HTTP_TOKEN/ERR_HTTP2_HEADER_SINGLE_VALUEcodes and messages) plus name lowercasing and the full tchar set accepted byis_lower_tchar.test/js/node/http/node-http-server-socket-surface.test.ts: pins the stub members installed byinstallSocketStubs(readyState, pending, bufferSize, ref/unref/setNoDelay return values, remote address properties); every assertion also holds under real Node.These tests pass on main too, and that is the point: this PR is a pure dedupe whose contract is byte-equivalent behavior, verified by an arm-by-arm audit of each consolidated path against main (error message and return-value parity per call site, field-for-field identical struct construction, symmetric origin comparison, side-effect-free reorders only). A test that failed on main and passed here would mean the dedupe changed behavior, which would be a bug in this PR, not a feature to prove. The tests instead lock in the invariants the consolidation must preserve so future edits to the shared helpers cannot silently drift. The buffered-handshake WebSocket init path is already covered by the existing "WebSocket buffered handshake data" tests in
websocket-client-short-read.test.ts, and the redirect/keep-alive paths by the existing fetch suites.