Skip to content

Deduplicate http client and websocket init internals#32020

Open
alii wants to merge 7 commits into
mainfrom
claude/split/http
Open

Deduplicate http client and websocket init internals#32020
alii wants to merge 7 commits into
mainfrom
claude/split/http

Conversation

@alii

@alii alii commented Jun 9, 2026

Copy link
Copy Markdown
Member

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 via src/http_types/h2.rs, used by h2_frame_parser), and consolidates the JS FakeSocket/server-socket stub descriptors in src/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.

Merge resolution against #31584

The node:http2 inbound-engine rewrite required non-trivial resolution in h2_frame_parser.rs:

  • is_malformed_field_name / is_malformed_field_value became pub(crate) for the new h2/connection.rs caller. Resolution: is_malformed_field_name keeps pub(crate) visibility with the is_lower_tchar body; is_malformed_field_value is a pub(crate) use re-export of the shared bun_http_types::h2 definition; is_valid_header_value stays as a thin inversion wrapper for the one remaining caller in the rewrite's new push-stream path.
  • rst_stream and set_stream_context changed lookup semantics (the former now handles unknown stream ids via a direct RST write instead of throwing; the latter now updates the new sctx map and only best-effort touches the legacy stream). The stream_from_js_arg helper no longer fits, so both reverted to main's inline code. The other 8 call sites were verified unchanged on main and keep the helper.
  • The shared encode_value closure picked up node:http2: rewritten inbound engine, batched write path, server push, +290 node v26.3.0 tests (79% passing) #31584's is_index_like_name never-index fast-path (applied identically in both original arms).

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, exact ERR_HTTP2_INVALID_HEADER_VALUE / ERR_INVALID_HTTP_TOKEN / ERR_HTTP2_HEADER_SINGLE_VALUE codes and messages) plus name lowercasing and the full tchar set accepted by is_lower_tchar.
  • test/js/node/http/node-http-server-socket-surface.test.ts: pins the stub members installed by installSocketStubs (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.

Post-merge verification: node-http2.test.js 287 pass / 0 fail, h2-conformance.test.ts 23/23, websocket/fetch/proxy suites all green.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b3f1a476-b811-4958-a775-30984cc6d2c1

📥 Commits

Reviewing files that changed from the base of the PR and between 601cceb and abe1037.

📒 Files selected for processing (2)
  • src/js/node/_http_server.ts
  • src/runtime/api/bun/h2_frame_parser.rs
💤 Files with no reviewable changes (1)
  • src/runtime/api/bun/h2_frame_parser.rs

Walkthrough

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

Changes

HTTP/2 Validation Extraction and Client/Parser Consolidation

Layer / File(s) Summary
HTTP/2 Header Field Validation Contract
src/http_types/h2.rs
Introduces is_lower_tchar to validate HTTP/2 header-name characters and is_malformed_field_value to detect NUL/LF/CR in header values per RFC 9113.
HTTP Client Completion and Redirect Refactoring
src/http/lib.rs
Extracts shared progress-completion helper send_progress_update_inner for both HTTP/1.1 and multiplexed paths, refactors keep-alive pooling with closure-based borrowck and request-drain gating, and consolidates redirect URL parsing/normalization via apply_redirect_url and normalize_and_apply_redirect_url helpers.
WebSocket Client Initialization Consolidation
src/http_jsc/websocket_client.rs
Introduces new_ws helper for WebSocket<SSL> allocation with deflate initialization and finish_init helper for shared post-allocation setup (buffer capacity, polling ref, buffered-data adoption, C++ ref) used by both init and init_with_tunnel.
HTTP/2 Response Header Validation
src/http/h2_client/dispatch.rs
Refactors is_malformed_response_field to use wire::is_lower_tchar and replaces is_malformed_response_value with a re-export of the centralized wire::is_malformed_field_value.
HTTP/2 Frame Parser Stream and Header Refactoring
src/runtime/api/bun/h2_frame_parser.rs
Introduces stream_from_js_arg helper to consolidate stream ID validation and resolution across host functions. Updates header-name validation to use is_lower_tchar and header-value validation to use is_malformed_field_value. Refactors header encoding closure to thread err_name for consistent error reporting and improves error-handling paths for stream state and onStreamError dispatch.

Socket Stub Infrastructure Extraction and Application

Layer / File(s) Summary
Socket Stub Infrastructure and Installation
src/js/internal/http.ts
Introduces shared socket-stub prototype descriptors (connection state, bufferSize, pending, address/port/family accessors, lifecycle helpers) and exports installSocketStubs(SocketClass) helper to define those properties on a target class prototype.
Socket Stub Application to Client Implementations
src/js/internal/http/FakeSocket.ts, src/js/node/_http_server.ts
Applies installSocketStubs to FakeSocket and NodeHTTPServerSocket, removing previously inlined socket compatibility methods and delegating to centralized stub installation.

Suggested reviewers

  • Jarred-Sumner
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective: deduplicating shared initialization code across HTTP client and WebSocket implementations, which is the primary change reflected in the code consolidation across multiple files.
Description check ✅ Passed The description comprehensively addresses both required template sections: 'What does this PR do?' is thoroughly covered with detailed explanations of code consolidation, deduplication, and impact; 'How did you verify your code works?' is extensively documented with behavioral-equivalence audits, CI verification, test coverage details, and post-merge verification results.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@robobun

robobun commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator
Updated 8:28 PM PT - Jun 17th, 2026

@robobun, your commit abe1037 has 2 failures in Build #63270 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32020

That installs a local version of the PR into your bun-32020 executable, so you can run:

bun-32020 --bun

@alii

alii commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

@robobun adopt

@robobun

robobun commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Adopted. Merged main after the node:http2 inbound-engine rewrite (#31584); resolution details in the PR body. All affected suites green locally (node-http2.test.js 287/0, h2-conformance 23/23, websocket/fetch/proxy all pass). Final CI on build 63270: 285 of 286 jobs passed with zero test failures; the single red is one darwin 26 aarch64 shard whose test/package.json install failed on a corrupted puppeteer chrome-headless-shell cache plus an @swc/core-darwin-arm64 tarball fetch error (agent environment, unrelated to the diff). Ready for review.

@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)
src/http/lib.rs (1)

3671-3680: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep-alive reuse still misclassifies in-flight stream/sendfile uploads as drained.

request_side_drained only checks the unsent slice for HTTPRequestBody::Bytes(_); every other body kind returns true. That still lets an HTTP/1.1 socket be pooled after an early response while a Stream or Sendfile upload 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

📥 Commits

Reviewing files that changed from the base of the PR and between 09703da and 5ceae99.

📒 Files selected for processing (8)
  • src/http/h2_client/dispatch.rs
  • src/http/lib.rs
  • src/http_jsc/websocket_client.rs
  • src/http_types/h2.rs
  • src/js/internal/http.ts
  • src/js/internal/http/FakeSocket.ts
  • src/js/node/_http_server.ts
  • src/runtime/api/bun/h2_frame_parser.rs

Comment thread src/js/internal/http.ts
@robobun

robobun commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

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.

@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)
src/http/lib.rs (1)

4349-4358: ⚠️ Potential issue | 🟠 Major

Add validation of redirect URL scheme and length in apply_redirect_url.

The protocol-relative redirect path (lines 4690-4706) validates the URL length before normalization. The relative redirect path (else branch at 4709-4721) calls bun_url::join without a length check, then passes the result directly to apply_redirect_url. Since apply_redirect_url performs no validation, an adversarially long relative redirect can bypass the length limit that applies to protocol-relative redirects. Additionally, although WHATWG URL joining from an http/https base should preserve the scheme, explicit validation in apply_redirect_url closes the gap and ensures only http/https redirects are installed, matching the precondition enforcement on the protocol-relative path.

Proposed fix
    fn apply_redirect_url(&mut self, new_href: Vec<u8>) -> bool {
+       if new_href.len() > MAX_REDIRECT_URL_LENGTH {
+           return false; // or return Err if signature changes to Result
+       }
        // SAFETY: self-borrow — `new_href` is moved into `self.redirect`
        // below, which lives as long as `self` (≥ `'a`).
        let new_url: URL<'a> = unsafe { URL::parse(&new_href).erase_lifetime() };
+       let protocol = new_url.display_protocol();
+       if protocol != b"http" && protocol != b"https" {
+           return false; // or return Err if signature changes to Result
+       }
        let is_same_origin = strings::eql_case_insensitive_ascii(
            strings::without_trailing_slash(new_url.origin),
            strings::without_trailing_slash(self.url.origin),
            true,
        );
🤖 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 4349 - 4358, The apply_redirect_url method
needs to add two validations to prevent security issues: first, validate that
the redirect URL scheme is either http or https to match the precondition
enforcement in the protocol-relative redirect path, and second, validate the URL
length before installation to prevent adversarially long relative redirects from
bypassing the length limits that apply to protocol-relative redirects. Add these
checks at the beginning of apply_redirect_url before the URL is assigned to
self.url.

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/http/lib.rs`:
- Around line 1327-1335: The has_unsent_request_body method does not account for
sendfile uploads, which leave request_body() empty but still have data in
flight. When a sendfile is active in RequestStage::Body and a peer FIN arrives,
the function incorrectly returns false, allowing a graceful close instead of the
required reset. Add a check for active sendfile uploads (similar to the
is_streaming_request_body flag check) that returns true if HTTPRequestBody
contains a Sendfile variant, ensuring in-flight sendfile operations are treated
as unsent request bodies.

---

Outside diff comments:
In `@src/http/lib.rs`:
- Around line 4349-4358: The apply_redirect_url method needs to add two
validations to prevent security issues: first, validate that the redirect URL
scheme is either http or https to match the precondition enforcement in the
protocol-relative redirect path, and second, validate the URL length before
installation to prevent adversarially long relative redirects from bypassing the
length limits that apply to protocol-relative redirects. Add these checks at the
beginning of apply_redirect_url before the URL is assigned to self.url.
🪄 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: 6725f7a2-49e6-4035-a758-3f555701fd9a

📥 Commits

Reviewing files that changed from the base of the PR and between 5ceae99 and 601cceb.

📒 Files selected for processing (5)
  • src/http/lib.rs
  • src/http_jsc/websocket_client.rs
  • src/js/internal/http.ts
  • src/js/node/_http_server.ts
  • src/runtime/api/bun/h2_frame_parser.rs
💤 Files with no reviewable changes (1)
  • src/runtime/api/bun/h2_frame_parser.rs

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

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)

4349-4358: ⚠️ Potential issue | 🟠 Major

Add validation of redirect URL scheme and length in apply_redirect_url.

The protocol-relative redirect path (lines 4690-4706) validates the URL length before normalization. The relative redirect path (else branch at 4709-4721) calls bun_url::join without a length check, then passes the result directly to apply_redirect_url. Since apply_redirect_url performs no validation, an adversarially long relative redirect can bypass the length limit that applies to protocol-relative redirects. Additionally, although WHATWG URL joining from an http/https base should preserve the scheme, explicit validation in apply_redirect_url closes the gap and ensures only http/https redirects are installed, matching the precondition enforcement on the protocol-relative path.

Proposed fix
    fn apply_redirect_url(&mut self, new_href: Vec<u8>) -> bool {
+       if new_href.len() > MAX_REDIRECT_URL_LENGTH {
+           return false; // or return Err if signature changes to Result
+       }
        // SAFETY: self-borrow — `new_href` is moved into `self.redirect`
        // below, which lives as long as `self` (≥ `'a`).
        let new_url: URL<'a> = unsafe { URL::parse(&new_href).erase_lifetime() };
+       let protocol = new_url.display_protocol();
+       if protocol != b"http" && protocol != b"https" {
+           return false; // or return Err if signature changes to Result
+       }
        let is_same_origin = strings::eql_case_insensitive_ascii(
            strings::without_trailing_slash(new_url.origin),
            strings::without_trailing_slash(self.url.origin),
            true,
        );
🤖 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 4349 - 4358, The apply_redirect_url method
needs to add two validations to prevent security issues: first, validate that
the redirect URL scheme is either http or https to match the precondition
enforcement in the protocol-relative redirect path, and second, validate the URL
length before installation to prevent adversarially long relative redirects from
bypassing the length limits that apply to protocol-relative redirects. Add these
checks at the beginning of apply_redirect_url before the URL is assigned to
self.url.

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/http/lib.rs`:
- Around line 1327-1335: The has_unsent_request_body method does not account for
sendfile uploads, which leave request_body() empty but still have data in
flight. When a sendfile is active in RequestStage::Body and a peer FIN arrives,
the function incorrectly returns false, allowing a graceful close instead of the
required reset. Add a check for active sendfile uploads (similar to the
is_streaming_request_body flag check) that returns true if HTTPRequestBody
contains a Sendfile variant, ensuring in-flight sendfile operations are treated
as unsent request bodies.

---

Outside diff comments:
In `@src/http/lib.rs`:
- Around line 4349-4358: The apply_redirect_url method needs to add two
validations to prevent security issues: first, validate that the redirect URL
scheme is either http or https to match the precondition enforcement in the
protocol-relative redirect path, and second, validate the URL length before
installation to prevent adversarially long relative redirects from bypassing the
length limits that apply to protocol-relative redirects. Add these checks at the
beginning of apply_redirect_url before the URL is assigned to self.url.
🪄 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: 6725f7a2-49e6-4035-a758-3f555701fd9a

📥 Commits

Reviewing files that changed from the base of the PR and between 5ceae99 and 601cceb.

📒 Files selected for processing (5)
  • src/http/lib.rs
  • src/http_jsc/websocket_client.rs
  • src/js/internal/http.ts
  • src/js/node/_http_server.ts
  • src/runtime/api/bun/h2_frame_parser.rs
💤 Files with no reviewable changes (1)
  • src/runtime/api/bun/h2_frame_parser.rs
🛑 Comments failed to post (1)
src/http/lib.rs (1)

1327-1335: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat in-flight sendfile uploads as unsent request bodies.

HTTPRequestBody::Sendfile(_) leaves request_body() empty, so a peer FIN while sendfile is still in RequestStage::Body returns false here and HTTPContext::on_end can graceful-close instead of resetting. That reintroduces the queued-FIN-behind-unsent-body hazard this helper is meant to prevent.

Proposed fix
     pub fn has_unsent_request_body(&self) -> bool {
         if self.state.request_stage == RequestStage::Done {
             return false;
         }
-        if self.flags.is_streaming_request_body {
-            // More body chunks may still be produced by JS.
-            return true;
-        }
-        !self.request_body().is_empty()
+        match &self.state.original_request_body {
+            HTTPRequestBody::Stream(_) => {
+                // More body chunks may still be produced by JS.
+                true
+            }
+            HTTPRequestBody::Sendfile(_) => {
+                // Sendfile progress is tracked by the sendfile object, not
+                // `request_body`; until RequestStage::Done, bytes may remain.
+                true
+            }
+            HTTPRequestBody::Bytes(_) => !self.request_body().is_empty(),
+        }
     }
🤖 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 1327 - 1335, The has_unsent_request_body method
does not account for sendfile uploads, which leave request_body() empty but
still have data in flight. When a sendfile is active in RequestStage::Body and a
peer FIN arrives, the function incorrectly returns false, allowing a graceful
close instead of the required reset. Add a check for active sendfile uploads
(similar to the is_streaming_request_body flag check) that returns true if
HTTPRequestBody contains a Sendfile variant, ensuring in-flight sendfile
operations are treated as unsent request bodies.

@robobun

robobun commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

On the two new findings:

has_unsent_request_body (line 1327): this function was added to main by #32462 and merged in here unchanged; this PR does not touch it (git diff origin/main...HEAD -- src/http/lib.rs has zero lines in that function). It appears in the review range only because the range spans the merge commit. If the Sendfile case needs handling, that belongs on #32462 or a follow-up to it.

apply_redirect_url validation (line 4349): the helper preserves main's behavior exactly. On main the relative else-arm at 4776-4798 (the bun_url::join path) has neither a length check nor a scheme check either; the two MAX_REDIRECT_URL_LENGTH checks at 4662/4725 gate the absolute and //-relative arms on the input header, which is already bounded by the HTTP header-size limit well under 128 KB. The else-arm only runs when location contains no "://" so it is a relative reference, and WHATWG URL joining with an http/https base always yields an http/https result; a scheme guard there is a no-op. Adding either check in this PR would be a behavior change, which this dedupe deliberately avoids.

@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 didn't find any bugs, but this is a ~700-line refactor across the core HTTP client (keep-alive pooling, redirect handling), WebSocket init (unsafe FFI/ref-counting), and the HTTP/2 frame parser — worth a pass from someone who owns these subsystems to confirm the behavioral-equivalence claim.

Extended reasoning...

Overview

This PR deduplicates internals across four networking subsystems with a claimed net of −700 lines and zero behavior change: (1) src/http/lib.rs — extracts send_progress_update_inner (shared between HTTP/1.1 and multiplexed paths) and apply_redirect_url/normalize_and_apply_redirect_url (shared across the three Location-header arms); (2) src/http_jsc/websocket_client.rs — extracts new_ws and finish_init shared between init and init_with_tunnel; (3) src/http_types/h2.rs + src/http/h2_client/dispatch.rs + src/runtime/api/bun/h2_frame_parser.rs — centralizes is_lower_tchar/is_malformed_field_value, adds a stream_from_js_arg helper for ~9 host fns, and folds the array/single-value header-encode arms into one closure; (4) src/js/internal/http.ts + FakeSocket.ts + _http_server.ts — extracts installSocketStubs for the duplicated socket-compat prototype members. Two new test files pin the consolidated surface.

Security risks

The consolidated HTTP/2 header-name/value validators are byte-identical to the originals (same tchar set, same NUL/CR/LF rejection), so no relaxation of header-injection guards. The redirect helper preserves the erase_lifetime self-borrow pattern and the prev_redirect swap. The WebSocket finish_init keeps the same ref-count discipline (initial I/O ref from new_ws, C++ ref taken at the tail) and the mimalloc buffer-adoption contract. I don't see new attack surface, but the unsafe blocks moved around enough that a second pair of eyes on the soundness invariants is warranted.

Level of scrutiny

High. This is production-critical networking code: the fetch() keep-alive pool release path, redirect origin comparison, WebSocket FFI init with raw-pointer ref-counting, and the node:http2 frame parser. The PR is explicitly a no-behavior-change refactor, but verifying that requires checking each consolidated path field-by-field against the original — e.g., the encode_value closure threads err_name/encode_err_return to preserve per-arm error messages and return values, and reorders stream.state = CLOSED relative to set_context in the single-value arm to match the array arm; the write_stream host fn now reads close_arg.to_boolean() after the stream lookup instead of before (commented as side-effect-free). These look correct to me but are exactly the kind of subtle equivalence a domain owner should sign off on.

Other factors

The bug-hunting system found nothing. CodeRabbit's one concern (setter null-deref) was correctly identified as a verbatim move of pre-existing code and withdrawn. The PR adds tests that pass on main too (by design — they pin invariants, not new behavior). No CODEOWNERS entries cover these paths. The PR has not yet been reviewed by a human; cirospaciari is the suggested reviewer.

@robobun

robobun commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Agreed. The two subtle points called out in the extended reasoning (the set_context/state = CLOSED reorder and the close_arg.to_boolean() move after the stream lookup) match the equivalence audit documented in the PR body: set_context just swaps a Strong handle and never reads stream.state, and ToBoolean on a JSValue is side-effect free, so both are unobservable. The per-arm error-message and return-value threading in encode_value was likewise verified against the original inline code for each call site. This is waiting on a pass from someone who owns these paths; @cirospaciari would be a good pick.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants