Skip to content

Deduplicate http client and websocket init internals#32020

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

Deduplicate http client and websocket init internals#32020
alii wants to merge 5 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.

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.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

Possibly related PRs

  • oven-sh/bun#31129: Hardens HTTP/2 frame/header decoding to use the same is_lower_tchar and is_malformed_field_value validation helpers extracted in this PR.

Suggested reviewers

  • cirospaciari
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main focus of the changeset—consolidating and removing duplicate initialization code across HTTP client and WebSocket modules.
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.
Description check ✅ Passed The pull request description comprehensively covers what the PR does, the consolidation strategy, testing approach, and verification methodology.

✏️ 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 3:05 AM PT - Jun 10th, 2026

@robobun, your commit 601934f has some failures in Build #61716 (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. 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.

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

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