node:http/https/http2: raise Node v26.3.0 compat to ~94%, sync the upstream suites, and fix the Windows/macOS transport-layer teardown bugs they exposed - #32488
Conversation
|
Updated 8:23 PM PT - Jul 15th, 2026
❌ @cirospaciari, your commit a4267b5 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 32488That installs a local version of the PR into your bun-32488 --bun |
|
Found 17 issues this PR may fix:
🤖 Generated with Claude Code |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
4a12f3e to
d494014
Compare
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds Node-compatible HTTP/HTTPS/HTTP/2 features including SNI accessors, insecure parser mode, HTTP/1.1 pipelining with queued responses, request trailers, timeout enforcement, upgrade/tunnel handling, domain lifecycle tracking, and HTTP/2 protocol validation, plus 200+ conformance tests. Changes
Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 15
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/runtime/api/bun/h2_frame_parser.rs (1)
6089-6205:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReset
explicit_settingsfor each SETTINGS submission.
load_settings_from_js_value()seeds the mask fromself.explicit_settings, so once a bit is set it is serialized by every latersettings()call even when that key is absent. For example, afterheaderTableSizeis set once,settings({ maxFrameSize })still sendsSETTINGS_HEADER_TABLE_SIZE. Start the mask at0for the current options and commit that per-submission mask only after validation succeeds.Proposed fix
- let mut explicit_settings = self.explicit_settings.get(); + let mut explicit_settings = 0;🤖 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/runtime/api/bun/h2_frame_parser.rs` around lines 6089 - 6205, The issue is that explicit_settings is initialized by fetching the value from self.explicit_settings, which means previously set bits persist across multiple calls to load_settings_from_js_value(). This causes settings from previous submissions to be included in later submissions even when they are not present in the current options. Initialize explicit_settings to 0 instead of self.explicit_settings.get() so that each call starts with a fresh mask, and only the settings present in the current options parameter will have their corresponding bits set before the mask is committed back to self.explicit_settings.set(explicit_settings).src/js/node/http2.ts (1)
2947-2955:⚠️ Potential issue | 🟠 Major | ⚡ Quick winKeep FD ownership scoped to the file-response operation.
kOwnsFdis sticky stream state. IfrespondWithFile()fails andonErrorleaves the stream usable, a laterrespondWithFD()inheritskOwnsFd === true, causing caller-owned descriptors to be closed or passed withautoClose: true.Proposed fix
-function doSendFileFD(options, fd, headers, err, stat) { +function doSendFileFD(options, ownsFd, fd, headers, err, stat) { const onError = options.onError; - const ownsFd = this[kOwnsFd] === true; if (err) { if (ownsFd && err.code !== "EBADF") { tryClose(fd); @@ - if (this[kOwnsFd] === true) tryClose(fd); + if (ownsFd) tryClose(fd); return; } @@ fd: fd, @@ - autoClose: this[kOwnsFd] === true, + autoClose: ownsFd, @@ - if (ownsFd) fileStream.destroy(); + if (ownsFd) fileStream.destroy();- this[kOwnsFd] = true; fs.open(path, "r", afterOpen.bind(this, options || {}, headers));- fs.fstat(fd, doSendFileFD.bind(this, options, fd, headers)); + fs.fstat(fd, doSendFileFD.bind(this, options, true, fd, headers));- fs.fstat(fd.fd, doSendFileFD.bind(this, options, fd, headers)); + fs.fstat(fd.fd, doSendFileFD.bind(this, options, false, fd, headers)); } else { - fs.fstat(fd, doSendFileFD.bind(this, options, fd, headers)); + fs.fstat(fd, doSendFileFD.bind(this, options, false, fd, headers)); }Also applies to: 3011-3016, 3050-3058, 3285-3286, 3344-3354
🤖 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/js/node/http2.ts` around lines 2947 - 2955, The kOwnsFd flag is persisting as sticky stream state across multiple file-response operations, causing subsequent calls like respondWithFD() to incorrectly inherit ownership settings from previous failed operations. In the doSendFileFD function and the related error handling paths at the locations mentioned (3011-3016, 3050-3058, 3285-3286, 3344-3354), ensure that kOwnsFd is reset to false or cleared after each file-response operation completes, whether it succeeds or fails. This will prevent the ownership flag from persisting and affecting later operations on the same stream, ensuring that each file-response operation has its own scoped FD ownership state rather than inheriting from previous operations.
🤖 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 `@packages/bun-uws/src/ChunkedEncoding.h`:
- Around line 132-142: The chunkExtensionsConsumed counter is being reset to 0
when a valid chunk-size line is found (in the return statement where
STATE_HAS_SIZE is set) before the caller has a chance to validate the
accumulated extensions. Add a bounds check at the increment point where
++*chunkExtensionsConsumed occurs to enforce the maximum extension size limit
before allowing further increments. If the counter would exceed the configured
maximum limit, return STATE_IS_ERROR to prevent bypassing the extension size
restriction when oversized extensions arrive within a single buffer.
- Around line 176-192: The MAX_TRAILER_SECTION_SIZE constant is hard-coded to 16
KiB, but trailers should be validated against the server's configured
maxHeaderSize limit instead. Modify the getNextChunk() function signature to
accept the active header limit as a parameter, and thread this limit through to
ChunkIterator as well. When validating the trailerSection size, replace the
comparison against the hard-coded MAX_TRAILER_SECTION_SIZE with a comparison
against the passed-in header limit value to ensure external protocol lengths are
bounded by the validated per-server configuration.
In `@src/js/internal/http.ts`:
- Line 30: The onConnection property uses an optional marker (?) which allows
TypeScript callers to omit passing the 7th argument, but the native binding
jsHTTPSetCustomOptions requires exactly 7 arguments to be passed. Remove the
optional marker from onConnection and instead make the type union explicitly
allow undefined, changing from onConnection?: (socketHandle: any) => undefined
to onConnection: ((socketHandle: any) => undefined) | undefined. This ensures
callers must always provide the parameter slot while still allowing undefined as
a valid value.
In `@src/js/node/_http_server.ts`:
- Around line 1124-1127: The Server.prototype.setTimeout method directly assigns
the msecs parameter without validating it first. Add validation to ensure msecs
is a non-negative integer before assigning it to this.timeout. If msecs fails
validation (such as being negative, NaN, or non-numeric), throw an appropriate
error with a descriptive message to match Node.js behavior and prevent
unexpected runtime issues.
- Around line 2178-2262: The try-finally block in the advanceResponsePipeline
function that replays buffered operations lacks error handling. If a write() or
end() call throws during the replay loop, the error will propagate uncaught and
leave the socket in an inconsistent state. Add a catch block to the existing
try-finally structure that wraps the ops replay loop, and within the catch
block, destroy both the response and socket to gracefully handle the error and
prevent further operations on the compromised connection.
In `@src/js/node/domain.ts`:
- Around line 60-69: The `run` method in the domain class needs to be modified
to properly handle callback arguments and return values. Update the method
signature to accept variadic arguments, forward all arguments when invoking the
callback function `fn`, and return the result of the callback execution instead
of always returning `this`. Ensure the callback is executed with the domain
bound as `this` context, and preserve the actual return value from the callback
to maintain Node.js compatibility.
In `@src/js/node/http2.ts`:
- Around line 4262-4269: The socket binding in the code snippet sets
socket[kBoundSession] before later fallible work completes, which can poison the
socket if construction fails. Move the socket binding statement to occur after
all validation and initialization work has succeeded, and add error handling to
clear the binding if any subsequent operations throw. Additionally, apply this
same transactional binding pattern to ClientHttp2Session by using
bindHttp2SessionSocket(socket, this) in the appropriate locations where sockets
are attached in ClientHttp2Session, ensuring consistency in how both server and
client sessions safely bind sockets.
- Around line 2604-2611: The line `code = code || 0` uses a logical OR operator
that coerces all falsy values (NaN, null, false, empty string) to 0 before
validation runs, allowing invalid input to pass validation as NGHTTP2_NO_ERROR.
Replace this permissive default assignment with a strict nullish coalescing
operator or explicit check that only defaults undefined or null to 0, ensuring
that invalid caller input like NaN or false is properly caught by the
validateInteger call that follows.
- Around line 5761-5769: The current condition in the signal validation block
checks both that options is an object AND that options.signal is truthy before
calling validateAbortSignal(). This causes falsey signal values like null,
false, or 0 to bypass validation entirely. Modify the condition to check if
options.signal exists as a property (using hasOwnProperty or similar) rather
than relying on truthiness, so that validateAbortSignal() is called for any
provided signal value including falsey ones, allowing the validation function to
properly reject invalid types.
- Around line 4618-4627: The current condition for detecting outbound progress
in the HTTP/2 timeout logic only refreshes the timeout when
sessionHasPendingWrite is true or nativeBuffered is greater than 0. When
nativeBuffered drains to zero (from a previously non-zero value), the code skips
the comparison between nativeBuffered and session[kTimeoutBytesSnapshot],
causing the timeout to potentially fire immediately despite data draining during
the interval. Fix this by also checking if nativeBuffered has changed from the
snapshot value (session[kTimeoutBytesSnapshot]) even when nativeBuffered equals
zero, and if it has changed, update the snapshot by setting
session[kTimeoutBytesSnapshot] = nativeBuffered and call
session[kTimeout]?.refresh() to properly treat the draining of buffered bytes as
outbound progress.
In `@src/js/node/tls.ts`:
- Around line 462-474: The issue is that getAllowUnauthorized() and
rejectUnauthorizedDefault() handle the NODE_TLS_REJECT_UNAUTHORIZED environment
variable inconsistently: getAllowUnauthorized() only treats "0" as disabling
verification and emits a warning, while rejectUnauthorizedDefault() also treats
"false" as disabling verification without a warning. To fix this, modify
rejectUnauthorizedDefault() to use !getAllowUnauthorized() as its implementation
instead of directly checking the environment variable, which ensures both
functions use the same single source of truth and consistently emit warnings for
all cases where verification is disabled.
In `@src/runtime/api/bun/h2_frame_parser.rs`:
- Around line 6204-6207: The settings parsing is not transactional because
local_settings and explicit_settings are being set into their cells via
self.local_settings.set() and self.explicit_settings.set() before customSettings
is fully validated. If customSettings validation fails, the earlier mutations
persist and can cause incorrect state in subsequent calls. Parse all settings
including customSettings validation into local temporaries first, then only
after all validation succeeds, commit them to the cells by calling
self.local_settings.set(), self.explicit_settings.set(), and writing to
self.custom_settings. Apply this same pattern to all locations where settings
are mutated (including the range mentioned at 6269-6276).
- Around line 5845-5852: Extract the byte-based session memory comparison logic
into a shared helper method, e.g. `is_over_session_memory_limit()`, that returns
a boolean. Replace the inline comparison in the `on_stream_open()` function with
a call to this helper. Apply this same helper consistently to all other
max-session-memory rejection paths mentioned in the applicable range (around
lines 7021-7031) to ensure uniform byte-based limit checking throughout the
code. Keep the existing `get_session_memory_usage()` method available for
logging and reporting purposes only, avoiding its use in budget gate decisions
where the MiB flooring could permit nearly one extra megabyte over the
configured limit.
In `@src/runtime/api/bun/h2/connection.rs`:
- Around line 707-730: In the note_outbound_ack method, add a brief inline
comment above or next to the condition check `self.obq_ack_pending <
MAX_OUTBOUND_ACK_QUEUE` to clarify that the flood error is intentionally
triggered when the 1000th ACK is queued (not 1001st) to match nghttp2's
behavior. The comment should explain that the counter is incremented before the
comparison, so the check fires on reaching the threshold value, documenting this
intentional off-by-one behavior for nghttp2 parity.
In `@src/runtime/server/server_body.rs`:
- Around line 3535-3555: The on_connection_callback method invokes
callback.call() with no mechanism to keep the server alive from the JS side,
creating a potential use-after-free if the callback re-entrantly disposes the
server. Add an RAII ref/deref guard using scopeguard before the callback.call()
invocation in on_connection_callback to pin the server lifetime, incrementing a
reference count before the call and decrementing on scope exit. Follow the same
pattern as used in on_upgrade_callback (referenced above) to ensure consistent
protection.
---
Outside diff comments:
In `@src/js/node/http2.ts`:
- Around line 2947-2955: The kOwnsFd flag is persisting as sticky stream state
across multiple file-response operations, causing subsequent calls like
respondWithFD() to incorrectly inherit ownership settings from previous failed
operations. In the doSendFileFD function and the related error handling paths at
the locations mentioned (3011-3016, 3050-3058, 3285-3286, 3344-3354), ensure
that kOwnsFd is reset to false or cleared after each file-response operation
completes, whether it succeeds or fails. This will prevent the ownership flag
from persisting and affecting later operations on the same stream, ensuring that
each file-response operation has its own scoped FD ownership state rather than
inheriting from previous operations.
In `@src/runtime/api/bun/h2_frame_parser.rs`:
- Around line 6089-6205: The issue is that explicit_settings is initialized by
fetching the value from self.explicit_settings, which means previously set bits
persist across multiple calls to load_settings_from_js_value(). This causes
settings from previous submissions to be included in later submissions even when
they are not present in the current options. Initialize explicit_settings to 0
instead of self.explicit_settings.get() so that each call starts with a fresh
mask, and only the settings present in the current options parameter will have
their corresponding bits set before the mask is committed back to
self.explicit_settings.set(explicit_settings).
🪄 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: 2f235446-1cab-41b7-b484-9bcd9aa45cb0
📒 Files selected for processing (237)
packages/bun-usockets/src/context.cpackages/bun-usockets/src/crypto/openssl.cpackages/bun-usockets/src/internal/internal.hpackages/bun-usockets/src/libusockets.hpackages/bun-uws/src/App.hpackages/bun-uws/src/ChunkedEncoding.hpackages/bun-uws/src/HttpContext.hpackages/bun-uws/src/HttpContextData.hpackages/bun-uws/src/HttpErrors.hpackages/bun-uws/src/HttpParser.hpackages/bun-uws/src/HttpResponse.hpackages/bun-uws/src/HttpResponseData.hsrc/js/internal/http.tssrc/js/internal/timers.tssrc/js/internal/tls.tssrc/js/node/_http_client.tssrc/js/node/_http_common.tssrc/js/node/_http_incoming.tssrc/js/node/_http_outgoing.tssrc/js/node/_http_server.tssrc/js/node/domain.tssrc/js/node/http2.tssrc/js/node/https.tssrc/js/node/net.tssrc/js/node/tls.tssrc/jsc/ErrorCode.rssrc/jsc/bindings/ErrorCode.tssrc/jsc/bindings/NodeHTTP.cppsrc/jsc/bindings/node/JSNodeHTTPServerSocket.cppsrc/jsc/bindings/node/JSNodeHTTPServerSocket.hsrc/jsc/bindings/node/JSNodeHTTPServerSocketPrototype.cppsrc/runtime/api/bun/h2/connection.rssrc/runtime/api/bun/h2/wire.rssrc/runtime/api/bun/h2_frame_parser.rssrc/runtime/api/h2.classes.tssrc/runtime/server/mod.rssrc/runtime/server/server_body.rssrc/uws_sys/App.rssrc/uws_sys/libuwsockets.cpptest/expectations.txttest/js/bun/test/parallel/test-http-host-array-should-throw-in-request.tstest/js/bun/test/parallel/test-http-should-emit-timeout-event-when-using-server-setTimeout.tstest/js/bun/test/parallel/test-http-should-emit-timeout-event.tstest/js/node/http/node-http.test.tstest/js/node/http2/h2-conformance.test.tstest/js/node/http2/node-http2.test.jstest/js/node/net/node-net.test.tstest/js/node/test/common/index.jstest/js/node/test/parallel/test-http-abort-stream-end.jstest/js/node/test/parallel/test-http-agent-domain-reused-gc.jstest/js/node/test/parallel/test-http-agent-keepalive-delay.jstest/js/node/test/parallel/test-http-agent-maxtotalsockets.jstest/js/node/test/parallel/test-http-agent-remove.jstest/js/node/test/parallel/test-http-allow-content-length-304.jstest/js/node/test/parallel/test-http-autoselectfamily.jstest/js/node/test/parallel/test-http-buffer-sanity.jstest/js/node/test/parallel/test-http-chunk-extensions-limit.jstest/js/node/test/parallel/test-http-chunk-problem.jstest/js/node/test/parallel/test-http-client-abort-keep-alive-queued-unix-socket.jstest/js/node/test/parallel/test-http-client-abort-unix-socket.jstest/js/node/test/parallel/test-http-client-close-with-default-agent.jstest/js/node/test/parallel/test-http-client-finished.jstest/js/node/test/parallel/test-http-client-immediate-error.jstest/js/node/test/parallel/test-http-client-keep-alive-hint.jstest/js/node/test/parallel/test-http-client-pipe-end.jstest/js/node/test/parallel/test-http-client-reject-unexpected-agent.jstest/js/node/test/parallel/test-http-client-request-options.jstest/js/node/test/parallel/test-http-client-response-domain.jstest/js/node/test/parallel/test-http-client-response-timeout.jstest/js/node/test/parallel/test-http-client-spurious-aborted.jstest/js/node/test/parallel/test-http-client-timeout-event.jstest/js/node/test/parallel/test-http-client-timeout-on-connect.jstest/js/node/test/parallel/test-http-client-timeout-option.jstest/js/node/test/parallel/test-http-client-timeout.jstest/js/node/test/parallel/test-http-client-with-create-connection.jstest/js/node/test/parallel/test-http-connect-req-res.jstest/js/node/test/parallel/test-http-connect.jstest/js/node/test/parallel/test-http-content-length-mismatch.jstest/js/node/test/parallel/test-http-correct-hostname.jstest/js/node/test/parallel/test-http-date-header.jstest/js/node/test/parallel/test-http-decoded-auth.jstest/js/node/test/parallel/test-http-double-content-length.jstest/js/node/test/parallel/test-http-dump-req-when-res-ends.jstest/js/node/test/parallel/test-http-early-hints-invalid-argument.jstest/js/node/test/parallel/test-http-end-throw-socket-handling.jstest/js/node/test/parallel/test-http-expect-handling.jstest/js/node/test/parallel/test-http-extra-response.jstest/js/node/test/parallel/test-http-flush-headers.jstest/js/node/test/parallel/test-http-flush-response-headers.jstest/js/node/test/parallel/test-http-generic-streams.jstest/js/node/test/parallel/test-http-head-throw-on-response-body-write.jstest/js/node/test/parallel/test-http-header-badrequest.jstest/js/node/test/parallel/test-http-header-obstext.jstest/js/node/test/parallel/test-http-header-read.jstest/js/node/test/parallel/test-http-header-value-relaxed.jstest/js/node/test/parallel/test-http-highwatermark.jstest/js/node/test/parallel/test-http-host-headers.jstest/js/node/test/parallel/test-http-hostname-typechecking.jstest/js/node/test/parallel/test-http-incoming-pipelined-socket-destroy.jstest/js/node/test/parallel/test-http-insecure-parser-per-stream.jstest/js/node/test/parallel/test-http-insecure-parser.jstest/js/node/test/parallel/test-http-invalidheaderfield.jstest/js/node/test/parallel/test-http-invalidheaderfield2.jstest/js/node/test/parallel/test-http-keep-alive-drop-requests.jstest/js/node/test/parallel/test-http-keep-alive-empty-line.mjstest/js/node/test/parallel/test-http-keep-alive-max-requests.jstest/js/node/test/parallel/test-http-localaddress.jstest/js/node/test/parallel/test-http-many-ended-pipelines.jstest/js/node/test/parallel/test-http-max-header-size-per-stream.jstest/js/node/test/parallel/test-http-max-http-headers.jstest/js/node/test/parallel/test-http-multiple-headers.jstest/js/node/test/parallel/test-http-no-read-no-dump.jstest/js/node/test/parallel/test-http-outgoing-drain-writable-length.jstest/js/node/test/parallel/test-http-outgoing-finished.jstest/js/node/test/parallel/test-http-outgoing-proto.jstest/js/node/test/parallel/test-http-outgoing-renderHeaders.jstest/js/node/test/parallel/test-http-parser-finish-error.jstest/js/node/test/parallel/test-http-parser-free.jstest/js/node/test/parallel/test-http-parser-freed-before-upgrade.jstest/js/node/test/parallel/test-http-parser-freed-during-execute.jstest/js/node/test/parallel/test-http-parser-memory-retention.jstest/js/node/test/parallel/test-http-parser-multiple-execute.jstest/js/node/test/parallel/test-http-parser-timeout-reset.jstest/js/node/test/parallel/test-http-parser.jstest/js/node/test/parallel/test-http-pause.jstest/js/node/test/parallel/test-http-pipeline-assertionerror-finish.jstest/js/node/test/parallel/test-http-pipeline-flood.jstest/js/node/test/parallel/test-http-pipeline-outgoing-destroy.jstest/js/node/test/parallel/test-http-proxy.jstest/js/node/test/parallel/test-http-raw-headers.jstest/js/node/test/parallel/test-http-readable-data-event.jstest/js/node/test/parallel/test-http-req-close-robust-from-tampering.jstest/js/node/test/parallel/test-http-req-res-close.jstest/js/node/test/parallel/test-http-request-end-twice.jstest/js/node/test/parallel/test-http-request-end.jstest/js/node/test/parallel/test-http-request-method-delete-payload.jstest/js/node/test/parallel/test-http-response-add-header-after-sent.jstest/js/node/test/parallel/test-http-response-readable.jstest/js/node/test/parallel/test-http-response-remove-header-after-sent.jstest/js/node/test/parallel/test-http-response-setheaders.jstest/js/node/test/parallel/test-http-response-status-message.jstest/js/node/test/parallel/test-http-response-statuscode.jstest/js/node/test/parallel/test-http-response-writehead-returns-this.jstest/js/node/test/parallel/test-http-same-map.jstest/js/node/test/parallel/test-http-server-client-error.jstest/js/node/test/parallel/test-http-server-close-all.jstest/js/node/test/parallel/test-http-server-close-idle-wait-response.jstest/js/node/test/parallel/test-http-server-close-idle.jstest/js/node/test/parallel/test-http-server-connection-list-when-close.jstest/js/node/test/parallel/test-http-server-destroy-socket-on-client-error.jstest/js/node/test/parallel/test-http-server-drop-connections-in-cluster.jstest/js/node/test/parallel/test-http-server-headers-timeout-delayed-headers.jstest/js/node/test/parallel/test-http-server-headers-timeout-interrupted-headers.jstest/js/node/test/parallel/test-http-server-headers-timeout-keepalive.jstest/js/node/test/parallel/test-http-server-headers-timeout-pipelining.jstest/js/node/test/parallel/test-http-server-keep-alive-timeout.jstest/js/node/test/parallel/test-http-server-keepalive-end.jstest/js/node/test/parallel/test-http-server-method.query.jstest/js/node/test/parallel/test-http-server-multiheaders.jstest/js/node/test/parallel/test-http-server-multiple-client-error.jstest/js/node/test/parallel/test-http-server-non-utf8-header.jstest/js/node/test/parallel/test-http-server-options-highwatermark.jstest/js/node/test/parallel/test-http-server-options-incoming-message.jstest/js/node/test/parallel/test-http-server-options-server-response.jstest/js/node/test/parallel/test-http-server-reject-chunked-with-content-length.jstest/js/node/test/parallel/test-http-server-reject-cr-no-lf.jstest/js/node/test/parallel/test-http-server-request-timeout-delayed-body.jstest/js/node/test/parallel/test-http-server-request-timeout-delayed-headers.jstest/js/node/test/parallel/test-http-server-request-timeout-interrupted-body.jstest/js/node/test/parallel/test-http-server-request-timeout-interrupted-headers.jstest/js/node/test/parallel/test-http-server-request-timeout-keepalive.jstest/js/node/test/parallel/test-http-server-request-timeout-pipelining.jstest/js/node/test/parallel/test-http-server-request-timeout-upgrade.jstest/js/node/test/parallel/test-http-server-stale-close.jstest/js/node/test/parallel/test-http-server-unconsume.jstest/js/node/test/parallel/test-http-server.jstest/js/node/test/parallel/test-http-set-cookies.jstest/js/node/test/parallel/test-http-set-header-chain.jstest/js/node/test/parallel/test-http-set-timeout-server.jstest/js/node/test/parallel/test-http-set-timeout.jstest/js/node/test/parallel/test-http-set-trailers.jstest/js/node/test/parallel/test-http-socket-encoding-error.jstest/js/node/test/parallel/test-http-status-code.jstest/js/node/test/parallel/test-http-status-message.jstest/js/node/test/parallel/test-http-timeout-overflow.jstest/js/node/test/parallel/test-http-transfer-encoding-repeated-chunked.jstest/js/node/test/parallel/test-http-unix-socket.jstest/js/node/test/parallel/test-http-upgrade-server-callback.jstest/js/node/test/parallel/test-http-upgrade-server-with-body-and-extras.mjstest/js/node/test/parallel/test-http-upgrade-server-with-body-error.mjstest/js/node/test/parallel/test-http-upgrade-server-with-body.mjstest/js/node/test/parallel/test-http-upgrade-server-with-large-body-unread.mjstest/js/node/test/parallel/test-http-upgrade-server-with-large-body.mjstest/js/node/test/parallel/test-http-url.parse-basic.jstest/js/node/test/parallel/test-http-url.parse-https.request.jstest/js/node/test/parallel/test-http-write-callbacks.jstest/js/node/test/parallel/test-http-zero-length-write.jstest/js/node/test/parallel/test-https-agent-additional-options.jstest/js/node/test/parallel/test-https-agent-keylog.jstest/js/node/test/parallel/test-https-agent-session-eviction.jstest/js/node/test/parallel/test-https-agent-sni.jstest/js/node/test/parallel/test-https-agent.jstest/js/node/test/parallel/test-https-argument-of-creating.jstest/js/node/test/parallel/test-https-autoselectfamily.jstest/js/node/test/parallel/test-https-byteswritten.jstest/js/node/test/parallel/test-https-client-renegotiation-limit.jstest/js/node/test/parallel/test-https-insecure-parse-per-stream.jstest/js/node/test/parallel/test-https-keep-alive-drop-requests.jstest/js/node/test/parallel/test-https-localaddress.jstest/js/node/test/parallel/test-https-max-header-size-per-stream.jstest/js/node/test/parallel/test-https-max-headers-count.jstest/js/node/test/parallel/test-https-options-boolean-check.jstest/js/node/test/parallel/test-https-pfx.jstest/js/node/test/parallel/test-https-resume-after-renew.jstest/js/node/test/parallel/test-https-selfsigned-no-keycertsign-no-crash.jstest/js/node/test/parallel/test-https-server-close-all.jstest/js/node/test/parallel/test-https-server-close-idle.jstest/js/node/test/parallel/test-https-set-timeout-server.jstest/js/node/test/parallel/test-https-strict.jstest/js/node/test/parallel/test-https-timeout-server-2.jstest/js/node/test/parallel/test-https-timeout-server.jstest/js/node/test/parallel/test-https-unix-socket-self-signed.jstest/js/node/test/parallel/test-tls-options-boolean-check.jstest/js/node/test/sequential/test-http-econnrefused.jstest/js/node/test/sequential/test-http-keep-alive-large-write.jstest/js/node/test/sequential/test-http-regr-gh-2928.jstest/js/node/test/sequential/test-http-server-keep-alive-timeout-slow-client-headers.jstest/js/node/test/sequential/test-http-server-keep-alive-timeout-slow-server.jstest/js/node/test/sequential/test-http-server-request-timeouts-mixed.jstest/js/node/test/sequential/test-http2-max-session-memory.jstest/js/node/test/sequential/test-http2-ping-flood.jstest/js/node/test/sequential/test-http2-settings-flood.jstest/js/node/test/sequential/test-http2-timeout-large-write-file.jstest/js/node/test/sequential/test-http2-timeout-large-write.jstest/js/node/test/sequential/test-https-connect-localport.jstest/js/node/test/sequential/test-https-server-keep-alive-timeout.jstest/regression/issue/25190.test.ts
💤 Files with no reviewable changes (5)
- test/js/node/test/parallel/test-http-client-with-create-connection.js
- test/js/node/test/parallel/test-http-client-response-timeout.js
- test/js/node/test/parallel/test-http-client-pipe-end.js
- test/js/node/test/parallel/test-http-unix-socket.js
- test/js/node/test/parallel/test-https-unix-socket-self-signed.js
d494014 to
d0b6fcc
Compare
There was a problem hiding this comment.
Actionable comments posted: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/js/node/http2.ts (1)
2949-3055:⚠️ Potential issue | 🟠 Major | ⚡ Quick winMake fd ownership per operation, not sticky stream state.
respondWithFile()setsthis[kOwnsFd] = true, butrespondWithFD()never clears it. If a file response fails or overlaps before headers are sent, a laterrespondWithFD()on the same stream can inherit ownership and close a caller-owned fd.Proposed fix
function doSendFileFD(options, fd, headers, err, stat) { const onError = options.onError; - const ownsFd = this[kOwnsFd] === true; + const ownsFd = options[kOwnsFd] === true; @@ - if (this[kOwnsFd] === true) tryClose(fd); + if (ownsFd) tryClose(fd); @@ - if (this[kOwnsFd] === true) tryClose(fd); + if (ownsFd) tryClose(fd); @@ - autoClose: this[kOwnsFd] === true, + autoClose: ownsFd, @@ - this[kOwnsFd] = true; + options[kOwnsFd] = true; fs.open(path, "r", afterOpen.bind(this, options || {}, headers));Also applies to: 3285-3286, 3344-3354
🤖 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/js/node/http2.ts` around lines 2949 - 3055, The fd ownership is currently stored as persistent stream state in `this[kOwnsFd]`, which causes a file descriptor opened by `respondWithFile()` to remain marked as owned even after that operation completes. If a subsequent `respondWithFD()` call fails before headers are sent, it will incorrectly close the caller-owned fd. Instead, make fd ownership per-operation by determining ownership locally at the start of the operation (where `respondWithFile()` sets ownership true and `respondWithFD()` sets it false) and storing this decision in a local variable. Replace all references to `this[kOwnsFd]` throughout the operation with this local ownership variable, and do not persist the ownership state back to the instance property after the operation completes.src/js/node/_http_server.ts (1)
2503-2511:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftBuffer informational responses while a pipelined response is queued.
Queued responses have
socket === null;writeProcessing()/writeEarlyHints()still reach_writeRaw()and can throw, whilewriteContinue()silently drops the100 Continue. Buffer_writeRaw/informational ops likewrite()andend()so they replay when the response gets the socket.Also applies to: 2597-2611
🤖 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/js/node/_http_server.ts` around lines 2503 - 2511, The _writeRaw method in ServerResponse.prototype needs to buffer write operations when the response has no socket assigned (socket === null for pipelined responses) instead of immediately executing them. Currently, informational responses from methods like writeProcessing() and writeEarlyHints() can throw or be silently dropped when reaching _writeRaw with a null socket. Implement buffering logic to queue the chunk, encoding, and callback when the socket is null, then replay these buffered operations once the response receives a socket assignment. Apply the same buffering logic to the corresponding methods mentioned at lines 2597-2611 to ensure all informational operations (write, end, etc.) are properly buffered and replayed.
🤖 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 `@packages/bun-uws/src/ChunkedEncoding.h`:
- Around line 207-218: The code in the STATE_IS_TRAILERS block accepts any bytes
matching the trailer end delimiter without validating their format as proper
header fields. Before transitioning the state to STATE_IS_TRAILERS_DONE and
emitting the final chunk, validate the trailerSection contents through a
header-specific parser that enforces token/value format validation and respects
the useInsecureHTTPParser setting, similar to how regular headers are validated.
This validation must occur immediately after isCompleteTrailerSection returns
true but before any state change or processing, ensuring malformed trailers are
rejected before req.trailers is exposed or the next pipelined request is parsed.
In `@packages/bun-uws/src/HttpContext.h`:
- Around line 329-334: The httpResponseData->lastMessageStartMs and
httpResponseData->headersCompleted state is not being reset for bodyless
requests (GET, HEAD, Content-Length: 0), causing incorrect timeout behavior on
slow async responses. Add logic to reset lastMessageStartMs to 0 and
headersCompleted to false when the parser determines there is no request body in
the httpContextData->flags.usingNodeHttpCompat block, matching the reset pattern
already present in the fin data path. Apply this same fix to both occurrences
mentioned (the main section and the also applies section around lines 456-462).
- Around line 727-736: The condition checking nodeHttpQueuedPipelinedCount only
preserves the socket when there are pipelined responses queued, but it fails to
account for the case where a current response is still pending. Modify the if
statement condition to also check whether an HTTP response is currently pending
(in addition to the existing nodeHttpQueuedPipelinedCount check) so that
half-closed sockets remain open both when responses are queued behind the
current one and when the current response itself is still being prepared by the
async server.
In `@packages/bun-uws/src/HttpResponseData.h`:
- Around line 151-184: The nodeHttpQueuedPipelinedCount counter declared as
uint16_t can overflow when more than 65535 pipelined responses are queued,
causing it to wrap around to 0 while responses remain queued, which breaks the
logic in shouldCloseConnection() and read-resumption checks. Fix this by either
widening nodeHttpQueuedPipelinedCount from uint16_t to uint32_t or size_t to
accommodate larger queue depths, or by adding saturation logic at the increment
site in HttpContext.h to ensure the counter never exceeds its maximum value (if
the counter is less than UINT16_MAX before incrementing, proceed with the
increment; otherwise, cap it at UINT16_MAX).
In `@src/js/internal/tls.ts`:
- Around line 111-145: The processPfxOptions function assigns parsed CA
certificates to the string key _pfxExtraCACerts only when pfxCAs has values,
which means user-provided _pfxExtraCACerts from the input options could be
spoofed and persist downstream. Either create a private exported symbol to use
instead of the string key _pfxExtraCACerts when storing the parsed CA values, or
unconditionally clear the _pfxExtraCACerts field from the output object before
conditionally assigning the parsed values to ensure downstream code only trusts
actual parsed CA material.
In `@src/js/node/_http_client.ts`:
- Around line 251-263: The httpValidation option is being validated and stored
in this.httpValidation but is not being wired into the actual parser
configuration, making it a no-op option. Find where the parser is being
initialized with the insecureHTTPParser flag and update that logic to also
consider the httpValidation value. Map the httpValidation options ("strict",
"relaxed", "insecure") to the appropriate parser leniency settings so that the
validation option actually affects the parsing behavior instead of just being
accepted and ignored.
In `@src/js/node/_http_outgoing.ts`:
- Around line 110-132: The `_isLenientHeaderValidation()` method treats any
non-"strict" value for `httpValidation` as lenient, but the server-side
`storeHTTPOptions()` function does not validate the `httpValidation` option
before it is stored. Add validation for `httpValidation` in `storeHTTPOptions()`
using `validateOneOf()` to restrict it to only the allowed values ["strict",
"relaxed", "insecure"], similar to how `insecureHTTPParser` is currently
validated. This ensures that only valid, explicitly allowed values are accepted
and prevents typos or garbage values from bypassing strict header validation.
In `@src/js/node/_http_server.ts`:
- Around line 711-728: The CONNECT request handling path contains early returns
before the HTTPS IncomingMessage flag is restored, causing the HTTPS state to
leak into subsequent request construction. In the CONNECT code path (around
lines 746-778), locate all early return statements and add a call to
setIsNextIncomingMessageHTTPS(prevIsNextIncomingMessageHTTPS) immediately before
each return to restore the previous HTTPS state, mirroring the restoration that
occurs at line 817 for the normal flow.
- Around line 2410-2417: The `writableCorked` property getter unconditionally
dereferences the socket, but for queued pipelined responses where `res.socket`
now returns `null`, this causes an error. Add a guard check in the
`writableCorked` getter (and the other related locations mentioned at lines
2450-2453) similar to the one in the socket getter that checks if
`this[kPipelinedQueuedState]` is undefined before accessing the socket. When it
is a queued pipelined response, return an appropriate default value (such as 0)
instead of attempting to dereference the socket.
- Around line 316-343: The ternary operator that merges pfxExtraCAs with ca
values uses the public Array.isArray function to check if ca is an array, but
this file should use the builtin-safe $isArray intrinsic instead for
tamper-resistance consistency. Replace Array.isArray(ca) with $isArray(ca) in
the ternary conditional expression on the line containing the ca assignment that
handles pfxExtraCAs merging.
- Around line 230-238: In the releaseServerParserShim function, replace the call
to parser.free() with a call to an internal no-op method instead. The
parser.free() method is exposed to userland and can be overwritten to throw
errors, which could prevent proper cleanup of parser.socket and tracked state
during close/upgrade operations. Use the internal implementation directly to
ensure cleanup always completes successfully.
- Around line 1172-1193: The isHttp2Preface function only checks the first 16
bytes of the HTTP/2 preface but the code reports bytesParsed = 24, creating a
mismatch. Extend the kHttp2PrefaceStart constant array to include the complete
24-byte HTTP/2 connection preface (the current 16-byte "PRI * HTTP/2.0\r\n"
string plus the additional 8 bytes that follow it), then ensure the
isHttp2Preface function validates against the full 24-byte preface before the
error is reported with bytesParsed = 24.
- Around line 2012-2016: In the header serialization conditional check, replace
the call to uniqueHeaders.has(key) with the builtin-safe version
uniqueHeaders.$has(key) to prevent user-overridable behavior. This ensures that
the membership check for uniqueHeaders uses the native Set method rather than a
potentially overridden one, in accordance with coding guidelines for built-in JS
modules.
In `@src/js/node/http2.ts`:
- Around line 150-155: The current conditional logic in the alias
synchronization block only handles cases where one of maxHeaderListSize or
maxHeaderSize is undefined, but when both are provided, they can end up with
different values despite representing the same SETTINGS id. Add an additional
condition to handle the case where both submitted.maxHeaderListSize and
submitted.maxHeaderSize are defined, and synchronize them to a single value
(such as the one that will be serialized) to ensure they remain aliased and
prevent an impossible local state where they differ.
In `@src/js/node/net.ts`:
- Around line 566-569: The code currently assigns an unwrapped SNI context to
state.selected without validating it is a legitimate SecureContext object. When
innerContext is extracted and truthy, add a validation check to ensure it is an
instance of state.server[kNativeSecureContextCtor] (similar to the check done in
the else-if branch for the direct context parameter) before assigning it to
state.selected. If the innerContext fails this validation, the code should fall
through to the else-if branch or handle it appropriately to prevent invalid
objects from bypassing the SecureContext type check.
- Around line 1057-1062: In the ConnResetException handling block where
listenerCount("error") is checked before calling self.destroy(er), add a
one-shot no-op listener to the "error" event before the destroy call. This
guards against the race condition where error listeners can be removed between
the listenerCount check and the deferred error emission. Install this temporary
listener using once("error", () => {}) pattern immediately before
self.destroy(er) to ensure there is always a listener to handle the error,
mirroring the approach used in the nearby SocketEmitEndNT reset path.
---
Outside diff comments:
In `@src/js/node/_http_server.ts`:
- Around line 2503-2511: The _writeRaw method in ServerResponse.prototype needs
to buffer write operations when the response has no socket assigned (socket ===
null for pipelined responses) instead of immediately executing them. Currently,
informational responses from methods like writeProcessing() and
writeEarlyHints() can throw or be silently dropped when reaching _writeRaw with
a null socket. Implement buffering logic to queue the chunk, encoding, and
callback when the socket is null, then replay these buffered operations once the
response receives a socket assignment. Apply the same buffering logic to the
corresponding methods mentioned at lines 2597-2611 to ensure all informational
operations (write, end, etc.) are properly buffered and replayed.
In `@src/js/node/http2.ts`:
- Around line 2949-3055: The fd ownership is currently stored as persistent
stream state in `this[kOwnsFd]`, which causes a file descriptor opened by
`respondWithFile()` to remain marked as owned even after that operation
completes. If a subsequent `respondWithFD()` call fails before headers are sent,
it will incorrectly close the caller-owned fd. Instead, make fd ownership
per-operation by determining ownership locally at the start of the operation
(where `respondWithFile()` sets ownership true and `respondWithFD()` sets it
false) and storing this decision in a local variable. Replace all references to
`this[kOwnsFd]` throughout the operation with this local ownership variable, and
do not persist the ownership state back to the instance property after the
operation completes.
🪄 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: 3bbe5b95-af18-4e69-b127-617f895e1f2e
📒 Files selected for processing (31)
packages/bun-usockets/src/context.cpackages/bun-usockets/src/crypto/openssl.cpackages/bun-usockets/src/internal/internal.hpackages/bun-usockets/src/libusockets.hpackages/bun-uws/src/App.hpackages/bun-uws/src/ChunkedEncoding.hpackages/bun-uws/src/HttpContext.hpackages/bun-uws/src/HttpContextData.hpackages/bun-uws/src/HttpErrors.hpackages/bun-uws/src/HttpParser.hpackages/bun-uws/src/HttpResponse.hpackages/bun-uws/src/HttpResponseData.hsrc/js/internal/http.tssrc/js/internal/timers.tssrc/js/internal/tls.tssrc/js/node/_http_client.tssrc/js/node/_http_common.tssrc/js/node/_http_incoming.tssrc/js/node/_http_outgoing.tssrc/js/node/_http_server.tssrc/js/node/domain.tssrc/js/node/http2.tssrc/js/node/https.tssrc/js/node/net.tssrc/js/node/tls.tssrc/jsc/ErrorCode.rssrc/jsc/bindings/ErrorCode.tssrc/jsc/bindings/NodeHTTP.cppsrc/jsc/bindings/node/JSNodeHTTPServerSocket.cppsrc/jsc/bindings/node/JSNodeHTTPServerSocket.hsrc/jsc/bindings/node/JSNodeHTTPServerSocketPrototype.cpp
💤 Files with no reviewable changes (6)
- src/jsc/bindings/ErrorCode.ts
- src/jsc/bindings/node/JSNodeHTTPServerSocketPrototype.cpp
- src/jsc/ErrorCode.rs
- src/jsc/bindings/NodeHTTP.cpp
- src/jsc/bindings/node/JSNodeHTTPServerSocket.h
- src/jsc/bindings/node/JSNodeHTTPServerSocket.cpp
There was a problem hiding this comment.
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: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/js/node/http2.ts (1)
2949-3055:⚠️ Potential issue | 🟠 Major | ⚡ Quick winMake fd ownership per operation, not sticky stream state.
respondWithFile()setsthis[kOwnsFd] = true, butrespondWithFD()never clears it. If a file response fails or overlaps before headers are sent, a laterrespondWithFD()on the same stream can inherit ownership and close a caller-owned fd.Proposed fix
function doSendFileFD(options, fd, headers, err, stat) { const onError = options.onError; - const ownsFd = this[kOwnsFd] === true; + const ownsFd = options[kOwnsFd] === true; @@ - if (this[kOwnsFd] === true) tryClose(fd); + if (ownsFd) tryClose(fd); @@ - if (this[kOwnsFd] === true) tryClose(fd); + if (ownsFd) tryClose(fd); @@ - autoClose: this[kOwnsFd] === true, + autoClose: ownsFd, @@ - this[kOwnsFd] = true; + options[kOwnsFd] = true; fs.open(path, "r", afterOpen.bind(this, options || {}, headers));Also applies to: 3285-3286, 3344-3354
🤖 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/js/node/http2.ts` around lines 2949 - 3055, The fd ownership is currently stored as persistent stream state in `this[kOwnsFd]`, which causes a file descriptor opened by `respondWithFile()` to remain marked as owned even after that operation completes. If a subsequent `respondWithFD()` call fails before headers are sent, it will incorrectly close the caller-owned fd. Instead, make fd ownership per-operation by determining ownership locally at the start of the operation (where `respondWithFile()` sets ownership true and `respondWithFD()` sets it false) and storing this decision in a local variable. Replace all references to `this[kOwnsFd]` throughout the operation with this local ownership variable, and do not persist the ownership state back to the instance property after the operation completes.src/js/node/_http_server.ts (1)
2503-2511:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftBuffer informational responses while a pipelined response is queued.
Queued responses have
socket === null;writeProcessing()/writeEarlyHints()still reach_writeRaw()and can throw, whilewriteContinue()silently drops the100 Continue. Buffer_writeRaw/informational ops likewrite()andend()so they replay when the response gets the socket.Also applies to: 2597-2611
🤖 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/js/node/_http_server.ts` around lines 2503 - 2511, The _writeRaw method in ServerResponse.prototype needs to buffer write operations when the response has no socket assigned (socket === null for pipelined responses) instead of immediately executing them. Currently, informational responses from methods like writeProcessing() and writeEarlyHints() can throw or be silently dropped when reaching _writeRaw with a null socket. Implement buffering logic to queue the chunk, encoding, and callback when the socket is null, then replay these buffered operations once the response receives a socket assignment. Apply the same buffering logic to the corresponding methods mentioned at lines 2597-2611 to ensure all informational operations (write, end, etc.) are properly buffered and replayed.
🤖 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 `@packages/bun-uws/src/ChunkedEncoding.h`:
- Around line 207-218: The code in the STATE_IS_TRAILERS block accepts any bytes
matching the trailer end delimiter without validating their format as proper
header fields. Before transitioning the state to STATE_IS_TRAILERS_DONE and
emitting the final chunk, validate the trailerSection contents through a
header-specific parser that enforces token/value format validation and respects
the useInsecureHTTPParser setting, similar to how regular headers are validated.
This validation must occur immediately after isCompleteTrailerSection returns
true but before any state change or processing, ensuring malformed trailers are
rejected before req.trailers is exposed or the next pipelined request is parsed.
In `@packages/bun-uws/src/HttpContext.h`:
- Around line 329-334: The httpResponseData->lastMessageStartMs and
httpResponseData->headersCompleted state is not being reset for bodyless
requests (GET, HEAD, Content-Length: 0), causing incorrect timeout behavior on
slow async responses. Add logic to reset lastMessageStartMs to 0 and
headersCompleted to false when the parser determines there is no request body in
the httpContextData->flags.usingNodeHttpCompat block, matching the reset pattern
already present in the fin data path. Apply this same fix to both occurrences
mentioned (the main section and the also applies section around lines 456-462).
- Around line 727-736: The condition checking nodeHttpQueuedPipelinedCount only
preserves the socket when there are pipelined responses queued, but it fails to
account for the case where a current response is still pending. Modify the if
statement condition to also check whether an HTTP response is currently pending
(in addition to the existing nodeHttpQueuedPipelinedCount check) so that
half-closed sockets remain open both when responses are queued behind the
current one and when the current response itself is still being prepared by the
async server.
In `@packages/bun-uws/src/HttpResponseData.h`:
- Around line 151-184: The nodeHttpQueuedPipelinedCount counter declared as
uint16_t can overflow when more than 65535 pipelined responses are queued,
causing it to wrap around to 0 while responses remain queued, which breaks the
logic in shouldCloseConnection() and read-resumption checks. Fix this by either
widening nodeHttpQueuedPipelinedCount from uint16_t to uint32_t or size_t to
accommodate larger queue depths, or by adding saturation logic at the increment
site in HttpContext.h to ensure the counter never exceeds its maximum value (if
the counter is less than UINT16_MAX before incrementing, proceed with the
increment; otherwise, cap it at UINT16_MAX).
In `@src/js/internal/tls.ts`:
- Around line 111-145: The processPfxOptions function assigns parsed CA
certificates to the string key _pfxExtraCACerts only when pfxCAs has values,
which means user-provided _pfxExtraCACerts from the input options could be
spoofed and persist downstream. Either create a private exported symbol to use
instead of the string key _pfxExtraCACerts when storing the parsed CA values, or
unconditionally clear the _pfxExtraCACerts field from the output object before
conditionally assigning the parsed values to ensure downstream code only trusts
actual parsed CA material.
In `@src/js/node/_http_client.ts`:
- Around line 251-263: The httpValidation option is being validated and stored
in this.httpValidation but is not being wired into the actual parser
configuration, making it a no-op option. Find where the parser is being
initialized with the insecureHTTPParser flag and update that logic to also
consider the httpValidation value. Map the httpValidation options ("strict",
"relaxed", "insecure") to the appropriate parser leniency settings so that the
validation option actually affects the parsing behavior instead of just being
accepted and ignored.
In `@src/js/node/_http_outgoing.ts`:
- Around line 110-132: The `_isLenientHeaderValidation()` method treats any
non-"strict" value for `httpValidation` as lenient, but the server-side
`storeHTTPOptions()` function does not validate the `httpValidation` option
before it is stored. Add validation for `httpValidation` in `storeHTTPOptions()`
using `validateOneOf()` to restrict it to only the allowed values ["strict",
"relaxed", "insecure"], similar to how `insecureHTTPParser` is currently
validated. This ensures that only valid, explicitly allowed values are accepted
and prevents typos or garbage values from bypassing strict header validation.
In `@src/js/node/_http_server.ts`:
- Around line 711-728: The CONNECT request handling path contains early returns
before the HTTPS IncomingMessage flag is restored, causing the HTTPS state to
leak into subsequent request construction. In the CONNECT code path (around
lines 746-778), locate all early return statements and add a call to
setIsNextIncomingMessageHTTPS(prevIsNextIncomingMessageHTTPS) immediately before
each return to restore the previous HTTPS state, mirroring the restoration that
occurs at line 817 for the normal flow.
- Around line 2410-2417: The `writableCorked` property getter unconditionally
dereferences the socket, but for queued pipelined responses where `res.socket`
now returns `null`, this causes an error. Add a guard check in the
`writableCorked` getter (and the other related locations mentioned at lines
2450-2453) similar to the one in the socket getter that checks if
`this[kPipelinedQueuedState]` is undefined before accessing the socket. When it
is a queued pipelined response, return an appropriate default value (such as 0)
instead of attempting to dereference the socket.
- Around line 316-343: The ternary operator that merges pfxExtraCAs with ca
values uses the public Array.isArray function to check if ca is an array, but
this file should use the builtin-safe $isArray intrinsic instead for
tamper-resistance consistency. Replace Array.isArray(ca) with $isArray(ca) in
the ternary conditional expression on the line containing the ca assignment that
handles pfxExtraCAs merging.
- Around line 230-238: In the releaseServerParserShim function, replace the call
to parser.free() with a call to an internal no-op method instead. The
parser.free() method is exposed to userland and can be overwritten to throw
errors, which could prevent proper cleanup of parser.socket and tracked state
during close/upgrade operations. Use the internal implementation directly to
ensure cleanup always completes successfully.
- Around line 1172-1193: The isHttp2Preface function only checks the first 16
bytes of the HTTP/2 preface but the code reports bytesParsed = 24, creating a
mismatch. Extend the kHttp2PrefaceStart constant array to include the complete
24-byte HTTP/2 connection preface (the current 16-byte "PRI * HTTP/2.0\r\n"
string plus the additional 8 bytes that follow it), then ensure the
isHttp2Preface function validates against the full 24-byte preface before the
error is reported with bytesParsed = 24.
- Around line 2012-2016: In the header serialization conditional check, replace
the call to uniqueHeaders.has(key) with the builtin-safe version
uniqueHeaders.$has(key) to prevent user-overridable behavior. This ensures that
the membership check for uniqueHeaders uses the native Set method rather than a
potentially overridden one, in accordance with coding guidelines for built-in JS
modules.
In `@src/js/node/http2.ts`:
- Around line 150-155: The current conditional logic in the alias
synchronization block only handles cases where one of maxHeaderListSize or
maxHeaderSize is undefined, but when both are provided, they can end up with
different values despite representing the same SETTINGS id. Add an additional
condition to handle the case where both submitted.maxHeaderListSize and
submitted.maxHeaderSize are defined, and synchronize them to a single value
(such as the one that will be serialized) to ensure they remain aliased and
prevent an impossible local state where they differ.
In `@src/js/node/net.ts`:
- Around line 566-569: The code currently assigns an unwrapped SNI context to
state.selected without validating it is a legitimate SecureContext object. When
innerContext is extracted and truthy, add a validation check to ensure it is an
instance of state.server[kNativeSecureContextCtor] (similar to the check done in
the else-if branch for the direct context parameter) before assigning it to
state.selected. If the innerContext fails this validation, the code should fall
through to the else-if branch or handle it appropriately to prevent invalid
objects from bypassing the SecureContext type check.
- Around line 1057-1062: In the ConnResetException handling block where
listenerCount("error") is checked before calling self.destroy(er), add a
one-shot no-op listener to the "error" event before the destroy call. This
guards against the race condition where error listeners can be removed between
the listenerCount check and the deferred error emission. Install this temporary
listener using once("error", () => {}) pattern immediately before
self.destroy(er) to ensure there is always a listener to handle the error,
mirroring the approach used in the nearby SocketEmitEndNT reset path.
---
Outside diff comments:
In `@src/js/node/_http_server.ts`:
- Around line 2503-2511: The _writeRaw method in ServerResponse.prototype needs
to buffer write operations when the response has no socket assigned (socket ===
null for pipelined responses) instead of immediately executing them. Currently,
informational responses from methods like writeProcessing() and
writeEarlyHints() can throw or be silently dropped when reaching _writeRaw with
a null socket. Implement buffering logic to queue the chunk, encoding, and
callback when the socket is null, then replay these buffered operations once the
response receives a socket assignment. Apply the same buffering logic to the
corresponding methods mentioned at lines 2597-2611 to ensure all informational
operations (write, end, etc.) are properly buffered and replayed.
In `@src/js/node/http2.ts`:
- Around line 2949-3055: The fd ownership is currently stored as persistent
stream state in `this[kOwnsFd]`, which causes a file descriptor opened by
`respondWithFile()` to remain marked as owned even after that operation
completes. If a subsequent `respondWithFD()` call fails before headers are sent,
it will incorrectly close the caller-owned fd. Instead, make fd ownership
per-operation by determining ownership locally at the start of the operation
(where `respondWithFile()` sets ownership true and `respondWithFD()` sets it
false) and storing this decision in a local variable. Replace all references to
`this[kOwnsFd]` throughout the operation with this local ownership variable, and
do not persist the ownership state back to the instance property after the
operation completes.
🪄 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: 3bbe5b95-af18-4e69-b127-617f895e1f2e
📒 Files selected for processing (31)
packages/bun-usockets/src/context.cpackages/bun-usockets/src/crypto/openssl.cpackages/bun-usockets/src/internal/internal.hpackages/bun-usockets/src/libusockets.hpackages/bun-uws/src/App.hpackages/bun-uws/src/ChunkedEncoding.hpackages/bun-uws/src/HttpContext.hpackages/bun-uws/src/HttpContextData.hpackages/bun-uws/src/HttpErrors.hpackages/bun-uws/src/HttpParser.hpackages/bun-uws/src/HttpResponse.hpackages/bun-uws/src/HttpResponseData.hsrc/js/internal/http.tssrc/js/internal/timers.tssrc/js/internal/tls.tssrc/js/node/_http_client.tssrc/js/node/_http_common.tssrc/js/node/_http_incoming.tssrc/js/node/_http_outgoing.tssrc/js/node/_http_server.tssrc/js/node/domain.tssrc/js/node/http2.tssrc/js/node/https.tssrc/js/node/net.tssrc/js/node/tls.tssrc/jsc/ErrorCode.rssrc/jsc/bindings/ErrorCode.tssrc/jsc/bindings/NodeHTTP.cppsrc/jsc/bindings/node/JSNodeHTTPServerSocket.cppsrc/jsc/bindings/node/JSNodeHTTPServerSocket.hsrc/jsc/bindings/node/JSNodeHTTPServerSocketPrototype.cpp
💤 Files with no reviewable changes (6)
- src/jsc/bindings/ErrorCode.ts
- src/jsc/bindings/node/JSNodeHTTPServerSocketPrototype.cpp
- src/jsc/ErrorCode.rs
- src/jsc/bindings/NodeHTTP.cpp
- src/jsc/bindings/node/JSNodeHTTPServerSocket.h
- src/jsc/bindings/node/JSNodeHTTPServerSocket.cpp
🛑 Comments failed to post (16)
packages/bun-uws/src/ChunkedEncoding.h (1)
207-218:
⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftValidate trailer fields before accepting the message.
This accepts any bytes ending in
\r\n\r\nas trailers and emits the final chunk without applying the header token/value validation used for regular headers. Malformed trailer fields should fail beforereq.trailersis exposed or the next pipelined request is parsed. Thread trailer bytes through a trailer-specific header parser that honorsuseInsecureHTTPParserand the active size limit. As per coding guidelines, “Validate untrusted input BEFORE any processing, allocation, or side effect.”🤖 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 `@packages/bun-uws/src/ChunkedEncoding.h` around lines 207 - 218, The code in the STATE_IS_TRAILERS block accepts any bytes matching the trailer end delimiter without validating their format as proper header fields. Before transitioning the state to STATE_IS_TRAILERS_DONE and emitting the final chunk, validate the trailerSection contents through a header-specific parser that enforces token/value format validation and respects the useInsecureHTTPParser setting, similar to how regular headers are validated. This validation must occur immediately after isCompleteTrailerSection returns true but before any state change or processing, ensuring malformed trailers are rejected before req.trailers is exposed or the next pipelined request is parsed.Source: Coding guidelines
packages/bun-uws/src/HttpContext.h (2)
329-334:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winClear request-timeout state for bodyless requests.
This starts the Node request-timeout window when headers complete, but the only visible completion reset is the
findata path.GET,HEAD, andContent-Length: 0requests can leavelastMessageStartMsnonzero after the full request is already received, so a slow async response may be timed out as if the request body were still arriving.Reset
lastMessageStartMsandheadersCompletedwhen the parser determines there is no request body, matching thefinpath.Also applies to: 456-462
🤖 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 `@packages/bun-uws/src/HttpContext.h` around lines 329 - 334, The httpResponseData->lastMessageStartMs and httpResponseData->headersCompleted state is not being reset for bodyless requests (GET, HEAD, Content-Length: 0), causing incorrect timeout behavior on slow async responses. Add logic to reset lastMessageStartMs to 0 and headersCompleted to false when the parser determines there is no request body in the httpContextData->flags.usingNodeHttpCompat block, matching the reset pattern already present in the fin data path. Apply this same fix to both occurrences mentioned (the main section and the also applies section around lines 456-462).
727-736:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winKeep half-closed sockets open while the current response is pending.
This preserves the socket only when pipelined responses are queued. A valid client can send a single request, half-close its write side, and still expect the async server response; with
HTTP_RESPONSE_PENDINGandnodeHttpQueuedPipelinedCount == 0, this falls through to the close path and can drop that response.Proposed fix
- if (httpResponseData->nodeHttpQueuedPipelinedCount > 0) { + if ((httpResponseData->state & HttpResponseData<SSL>::HTTP_RESPONSE_PENDING) + || httpResponseData->nodeHttpQueuedPipelinedCount > 0) { httpResponseData->nodeHttpReceivedFIN = true; return s; }🤖 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 `@packages/bun-uws/src/HttpContext.h` around lines 727 - 736, The condition checking nodeHttpQueuedPipelinedCount only preserves the socket when there are pipelined responses queued, but it fails to account for the case where a current response is still pending. Modify the if statement condition to also check whether an HTTP response is currently pending (in addition to the existing nodeHttpQueuedPipelinedCount check) so that half-closed sockets remain open both when responses are queued behind the current one and when the current response itself is still being prepared by the async server.packages/bun-uws/src/HttpResponseData.h (1)
151-184:
⚠️ Potential issue | 🔴 Critical🧩 Analysis chain
🏁 Script executed:
#!/bin/bash # Verify all mutation sites for nodeHttpQueuedPipelinedCount. rg -n -C4 '\bnodeHttpQueuedPipelinedCount\b'Repository: oven-sh/bun
Length of output: 6745
🏁 Script executed:
# Search for any constants or limits related to pipelined requests, queue size, or request buffering rg -i "max.*pipeline|pipeline.*max|pipelined.*limit|queue.*size|max.*queued" --type cpp --type hRepository: oven-sh/bun
Length of output: 3328
🏁 Script executed:
# Check if there's saturation logic or bounds checking before the increment at HttpContext.h:360 sed -n '340,380p' packages/bun-uws/src/HttpContext.hRepository: oven-sh/bun
Length of output: 2413
🏁 Script executed:
# Search for any requestor memory limits that might implicitly cap the queue rg "backlog|buffer.*limit|max.*request|request.*limit" packages/bun-uws/src/HttpContext.h -A 2Repository: oven-sh/bun
Length of output: 37
🏁 Script executed:
# Search for HTTP/request parsing limits or buffer constraints that might cap the queue rg "getBufferedAmount|backpressure|read.*pause|onData|onHeadersComplete" packages/bun-uws/src/HttpContext.h -B 2 -A 2 | head -60Repository: oven-sh/bun
Length of output: 3392
🏁 Script executed:
# Check if there are any validation checks or early returns that prevent excessive pipelined requests sed -n '320,370p' packages/bun-uws/src/HttpContext.h | cat -nRepository: oven-sh/bun
Length of output: 3514
🏁 Script executed:
# Search for any connection-level constants, limits, or initialization that might cap requests rg "struct.*HttpResponseData|class.*HttpResponseData" packages/bun-uws/src/ -A 30 | head -80Repository: oven-sh/bun
Length of output: 2726
🏁 Script executed:
# Check if there's any saturation or clamping logic around the increment sed -n '355,365p' packages/bun-uws/src/HttpContext.h | cat -nRepository: oven-sh/bun
Length of output: 785
🏁 Script executed:
# Search for any node:http request queueing or memory limits rg "nodeHttpQueuedPipelinedCount|pipelined.*queue" packages/bun-uws/src/ --type cpp --type h -B 1 -A 1Repository: oven-sh/bun
Length of output: 2194
🏁 Script executed:
# Check if socket buffer size provides an implicit practical cap rg "getBufferedAmount|BUFFER|buffer.*size|MAX" packages/bun-uws/src/HttpContext.h -i | head -30Repository: oven-sh/bun
Length of output: 2147
Use a wider counter or add saturation logic to prevent
nodeHttpQueuedPipelinedCountoverflow.The counter (uint16_t, max 65535) is incremented unconditionally at HttpContext.h:360 with no bounds check. If 65,536+ pipelined responses queue before draining—reachable if requests arrive faster than backpressure flushes—the counter wraps to 0 while responses remain queued. This causes
shouldCloseConnection()and read-resumption logic to misread the queue as empty, leading to premature connection closure or read resumption despite pending responses.Replace the bare increment with saturation (
if (count < UINT16_MAX) count++) or widen touint32_t/size_t.🤖 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 `@packages/bun-uws/src/HttpResponseData.h` around lines 151 - 184, The nodeHttpQueuedPipelinedCount counter declared as uint16_t can overflow when more than 65535 pipelined responses are queued, causing it to wrap around to 0 while responses remain queued, which breaks the logic in shouldCloseConnection() and read-resumption checks. Fix this by either widening nodeHttpQueuedPipelinedCount from uint16_t to uint32_t or size_t to accommodate larger queue depths, or by adding saturation logic at the increment site in HttpContext.h to ensure the counter never exceeds its maximum value (if the counter is less than UINT16_MAX before incrementing, proceed with the increment; otherwise, cap it at UINT16_MAX).src/js/internal/tls.ts (1)
111-145:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon’t keep internal PFX CA state on a user-spoofable string key.
_pfxExtraCACertsis copied from user options and only overwritten when parsed PFX CAs exist; downstream TLS code then trusts that field as parsed CA material. Store it on a private exported symbol, or clear it unconditionally before assigning parsed values.Suggested hardening
function processPfxOptions(options) { - if (options == null || options.pfx == null) return options; + if (options == null || options.pfx == null) return options; NativeSecureContext ??= $zig("SecureContext.zig", "js.getConstructor"); const out = { ...options }; + out._pfxExtraCACerts = undefined; const keys = out.key == null ? [] : Array.isArray(out.key) ? [...out.key] : [out.key]; const certs = out.cert == null ? [] : Array.isArray(out.cert) ? [...out.cert] : [out.cert]; const pfxCAs = [];A symbol shared through
internal/tlswould be stronger than a string key if consumers can be updated in this 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/js/internal/tls.ts` around lines 111 - 145, The processPfxOptions function assigns parsed CA certificates to the string key _pfxExtraCACerts only when pfxCAs has values, which means user-provided _pfxExtraCACerts from the input options could be spoofed and persist downstream. Either create a private exported symbol to use instead of the string key _pfxExtraCACerts when storing the parsed CA values, or unconditionally clear the _pfxExtraCACerts field from the output object before conditionally assigning the parsed values to ensure downstream code only trusts actual parsed CA material.src/js/node/_http_client.ts (1)
251-263:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winWire
httpValidationinto the parser flags.The constructor validates and stores
this.httpValidation, but the parser setup still derives leniency only fromreq.insecureHTTPParser;strict/relaxed/insecuretherefore risks becoming an accepted no-op option instead of changing response parsing behavior.🤖 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/js/node/_http_client.ts` around lines 251 - 263, The httpValidation option is being validated and stored in this.httpValidation but is not being wired into the actual parser configuration, making it a no-op option. Find where the parser is being initialized with the insecureHTTPParser flag and update that logic to also consider the httpValidation value. Map the httpValidation options ("strict", "relaxed", "insecure") to the appropriate parser leniency settings so that the validation option actually affects the parsing behavior instead of just being accepted and ignored.src/js/node/_http_outgoing.ts (1)
110-132:
⚠️ Potential issue | 🔴 Critical🧩 Analysis chain
🏁 Script executed:
#!/bin/bash # Description: Inspect all httpValidation writers/readers to confirm values are validated # before OutgoingMessage.prototype._isLenientHeaderValidation observes them. rg -nP -C4 --type=ts '\bhttpValidation\b'Repository: oven-sh/bun
Length of output: 3725
🏁 Script executed:
rg -nP 'httpValidation' src/js/node/_http_server.ts -A3 -B3Repository: oven-sh/bun
Length of output: 37
🏁 Script executed:
fd -t f '.*http_server.*' src/js/node/ && rg -nP 'httpValidation' src/js/node/ --type=tsRepository: oven-sh/bun
Length of output: 1434
🏁 Script executed:
cat -n src/js/node/_http_server.ts | head -300Repository: oven-sh/bun
Length of output: 11709
🏁 Script executed:
sed -n '300,450p' src/js/node/_http_server.tsRepository: oven-sh/bun
Length of output: 5082
🏁 Script executed:
rg -nP 'storeHTTPOptions' src/js/node/_http_server.ts -A20Repository: oven-sh/bun
Length of output: 1838
🏁 Script executed:
sed -n '3304,3360p' src/js/node/_http_server.tsRepository: oven-sh/bun
Length of output: 2433
🏁 Script executed:
sed -n '3360,3400p' src/js/node/_http_server.tsRepository: oven-sh/bun
Length of output: 1705
🏁 Script executed:
sed -n '3400,3450p' src/js/node/_http_server.tsRepository: oven-sh/bun
Length of output: 1281
ServerResponse path lacks validation for
httpValidation— arbitrary values will be treated as lenient.The server-side
storeHTTPOptions()does not validate thehttpValidationoption (onlyinsecureHTTPParseris validated), but the_isLenientHeaderValidation()helper readsthis.req?.socket?.server?.httpValidationand treats any non-"strict"value as lenient. This violates the fail-closed requirement: a typo,null, or any garbage value bypasses strict header validation.ClientRequest properly validates
httpValidationagainst["strict", "relaxed", "insecure"]before storage, but ServerResponse inherits an unvalidated value. AddvalidateOneOf()forhttpValidationinstoreHTTPOptions(), or explicitly allow-list only"relaxed"and"insecure"in the helper.🤖 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/js/node/_http_outgoing.ts` around lines 110 - 132, The `_isLenientHeaderValidation()` method treats any non-"strict" value for `httpValidation` as lenient, but the server-side `storeHTTPOptions()` function does not validate the `httpValidation` option before it is stored. Add validation for `httpValidation` in `storeHTTPOptions()` using `validateOneOf()` to restrict it to only the allowed values ["strict", "relaxed", "insecure"], similar to how `insecureHTTPParser` is currently validated. This ensures that only valid, explicitly allowed values are accepted and prevents typos or garbage values from bypassing strict header validation.Source: Coding guidelines
src/js/node/_http_server.ts (6)
230-238:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon’t call the user-mutable parser shim method during cleanup.
socket.parseris exposed to userland, soparser.free()can be overwritten and throw during close/upgrade cleanup beforeparser.socketand tracked state are released. Call the internal no-op directly instead.🛡️ Proposed fix
- parser.free(); + serverParserShimFree.$call(parser);🤖 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/js/node/_http_server.ts` around lines 230 - 238, In the releaseServerParserShim function, replace the call to parser.free() with a call to an internal no-op method instead. The parser.free() method is exposed to userland and can be overwritten to throw errors, which could prevent proper cleanup of parser.socket and tracked state during close/upgrade operations. Use the internal implementation directly to ensure cleanup always completes successfully.
316-343:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse the
$isArrayintrinsic for builtin tamper-resistance.Line 342 routes TLS option normalization through user-overridable
Array.isArray; this file already uses$isArrayelsewhere for builtin-safe checks. As per coding guidelines, built-in JS modules must use$-prefixed intrinsics/private APIs instead of public globals on internal paths.🛡️ Proposed fix
- ca = ca == null ? pfxExtraCAs : Array.isArray(ca) ? [...ca, ...pfxExtraCAs] : [ca, ...pfxExtraCAs]; + ca = ca == null ? pfxExtraCAs : $isArray(ca) ? [...ca, ...pfxExtraCAs] : [ca, ...pfxExtraCAs];🤖 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/js/node/_http_server.ts` around lines 316 - 343, The ternary operator that merges pfxExtraCAs with ca values uses the public Array.isArray function to check if ca is an array, but this file should use the builtin-safe $isArray intrinsic instead for tamper-resistance consistency. Replace Array.isArray(ca) with $isArray(ca) in the ternary conditional expression on the line containing the ca assignment that handles pfxExtraCAs merging.Source: Coding guidelines
711-728:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winRestore the HTTPS IncomingMessage flag before CONNECT early returns.
The CONNECT path returns before Line 817 restores
setIsNextIncomingMessageHTTPS(prevIsNextIncomingMessageHTTPS), leaking the HTTPS state into later request construction after an HTTPS CONNECT request.🐛 Proposed fix
- const http_req = new RequestClass(kHandle, url, method, headersObject, headersArray, handle, hasBody, socket); + let http_req; + try { + http_req = new RequestClass(kHandle, url, method, headersObject, headersArray, handle, hasBody, socket); + } finally { + setIsNextIncomingMessageHTTPS(prevIsNextIncomingMessageHTTPS); + } ... - setIsNextIncomingMessageHTTPS(prevIsNextIncomingMessageHTTPS);Also applies to: 746-778, 817-817
🤖 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/js/node/_http_server.ts` around lines 711 - 728, The CONNECT request handling path contains early returns before the HTTPS IncomingMessage flag is restored, causing the HTTPS state to leak into subsequent request construction. In the CONNECT code path (around lines 746-778), locate all early return statements and add a call to setIsNextIncomingMessageHTTPS(prevIsNextIncomingMessageHTTPS) immediately before each return to restore the previous HTTPS state, mirroring the restoration that occurs at line 817 for the normal flow.
1172-1193:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winMatch the full HTTP/2 preface before reporting 24 parsed bytes.
isHttp2Preface()only checks the first 16 bytes but then reportsbytesParsed = 24. This can classify a partial/prefix match asHPE_PAUSED_H2_UPGRADEwith an impossible parsed length.🐛 Proposed fix
const kHttp2PrefaceStart = [ 0x50, 0x52, 0x49, 0x20, 0x2a, 0x20, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x32, 0x2e, 0x30, 0x0d, 0x0a, -]; // "PRI * HTTP/2.0\r\n" + 0x0d, 0x0a, 0x53, 0x4d, 0x0d, 0x0a, 0x0d, 0x0a, +]; // "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"🤖 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/js/node/_http_server.ts` around lines 1172 - 1193, The isHttp2Preface function only checks the first 16 bytes of the HTTP/2 preface but the code reports bytesParsed = 24, creating a mismatch. Extend the kHttp2PrefaceStart constant array to include the complete 24-byte HTTP/2 connection preface (the current 16-byte "PRI * HTTP/2.0\r\n" string plus the additional 8 bytes that follow it), then ensure the isHttp2Preface function validates against the full 24-byte preface before the error is reported with bytesParsed = 24.
2012-2016:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse the Set intrinsic for
uniqueHeadersmembership checks.
uniqueHeaders.has(key)routes header serialization through a user-overridable Set method. UseuniqueHeaders.$has(key)for builtin-safe behavior. As per coding guidelines, built-in JS modules must use private$methods for internal Map/Set access.🛡️ Proposed fix
- if (valueLength >= 2 && (key === "cookie" || (uniqueHeaders != null && uniqueHeaders.has(key)))) { + if (valueLength >= 2 && (key === "cookie" || (uniqueHeaders != null && uniqueHeaders.$has(key)))) {🤖 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/js/node/_http_server.ts` around lines 2012 - 2016, In the header serialization conditional check, replace the call to uniqueHeaders.has(key) with the builtin-safe version uniqueHeaders.$has(key) to prevent user-overridable behavior. This ensures that the membership check for uniqueHeaders uses the native Set method rather than a potentially overridden one, in accordance with coding guidelines for built-in JS modules.Source: Coding guidelines
2410-2417:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winGuard
writableCorkedfor queued pipelined responses.
res.socketnow returnsnullwhile queued, butwritableCorkedstill dereferences it unconditionally. Readingres.writableCorkedin a pipelined handler can throw.🐛 Proposed fix
Object.defineProperty(ServerResponse.prototype, "writableCorked", { get() { - return this.socket.writableCorked; + return this.socket?.writableCorked ?? 0; }, set(_value) {}, });Also applies to: 2450-2453
🤖 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/js/node/_http_server.ts` around lines 2410 - 2417, The `writableCorked` property getter unconditionally dereferences the socket, but for queued pipelined responses where `res.socket` now returns `null`, this causes an error. Add a guard check in the `writableCorked` getter (and the other related locations mentioned at lines 2450-2453) similar to the one in the socket getter that checks if `this[kPipelinedQueuedState]` is undefined before accessing the socket. When it is a queued pipelined response, return an appropriate default value (such as 0) instead of attempting to dereference the socket.src/js/node/http2.ts (1)
150-155:
⚠️ Potential issue | 🟡 Minor | ⚡ Quick winKeep
maxHeaderSizeandmaxHeaderListSizealiased when both are provided.The loop can leave
localSettings.maxHeaderListSizeandlocalSettings.maxHeaderSizewith different pre-ACK values when both aliases are submitted. Since they represent the same SETTINGS id, mirror the value that will be serialized instead of exposing an impossible local state.Proposed fix
- if (submitted.maxHeaderListSize !== undefined && submitted.maxHeaderSize === undefined) { + if (submitted.maxHeaderListSize !== undefined) { target.maxHeaderSize = submitted.maxHeaderListSize; - } else if (submitted.maxHeaderSize !== undefined && submitted.maxHeaderListSize === undefined) { + } else if (submitted.maxHeaderSize !== undefined) { target.maxHeaderListSize = submitted.maxHeaderSize; }🤖 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/js/node/http2.ts` around lines 150 - 155, The current conditional logic in the alias synchronization block only handles cases where one of maxHeaderListSize or maxHeaderSize is undefined, but when both are provided, they can end up with different values despite representing the same SETTINGS id. Add an additional condition to handle the case where both submitted.maxHeaderListSize and submitted.maxHeaderSize are defined, and synchronize them to a single value (such as the one that will be serialized) to ensure they remain aliased and prevent an impossible local state where they differ.src/js/node/net.ts (2)
566-569:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winValidate the unwrapped SNI context before selecting it.
SNICallbackis user-controlled, and any object with a truthycontextproperty currently bypasses the nativeSecureContextcheck. That can pass a plain object/boolean into TLS resume instead of failing with"Invalid SNI context". As per coding guidelines, validate representation at every boundary.Proposed fix
- const innerContext = typeof context === "object" ? context.context : undefined; - if (innerContext) { + const NativeSecureContext = state.server?.[kNativeSecureContextCtor]; + const innerContext = typeof context === "object" ? context.context : undefined; + if (NativeSecureContext && innerContext instanceof NativeSecureContext) { state.selected = innerContext; - } else if (state.server?.[kNativeSecureContextCtor] && context instanceof state.server[kNativeSecureContextCtor]) { + } else if (NativeSecureContext && context instanceof NativeSecureContext) { state.selected = context; } else { state.failed = new Error("Invalid SNI context");🤖 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/js/node/net.ts` around lines 566 - 569, The code currently assigns an unwrapped SNI context to state.selected without validating it is a legitimate SecureContext object. When innerContext is extracted and truthy, add a validation check to ensure it is an instance of state.server[kNativeSecureContextCtor] (similar to the check done in the else-if branch for the direct context parameter) before assigning it to state.selected. If the innerContext fails this validation, the code should fall through to the else-if branch or handle it appropriately to prevent invalid objects from bypassing the SecureContext type check.Source: Coding guidelines
1057-1062:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winGuard the synthesized reset error against listener removal.
This branch checks
listenerCount("error")beforedestroy(er), but the error emission can still be deferred. Mirror the nearbySocketEmitEndNTreset path and install a one-shot no-op listener before destroying, otherwise a listener removed between the check and emission can still crash as an uncaught error.Proposed fix
if (self.listenerCount("error") > 0) { + self.once("error", () => {}); const er = new ConnResetException("read ECONNRESET") as Error & { errno?: number; syscall?: string }; er.errno = process.platform === "win32" ? -4077 : process.platform === "linux" ? -104 : -54; er.syscall = "read";🤖 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/js/node/net.ts` around lines 1057 - 1062, In the ConnResetException handling block where listenerCount("error") is checked before calling self.destroy(er), add a one-shot no-op listener to the "error" event before the destroy call. This guards against the race condition where error listeners can be removed between the listenerCount check and the deferred error emission. Install this temporary listener using once("error", () => {}) pattern immediately before self.destroy(er) to ensure there is always a listener to handle the error, mirroring the approach used in the nearby SocketEmitEndNT reset path.
d0b6fcc to
038565b
Compare
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/runtime/api/bun/h2_frame_parser.rs (1)
6237-6269: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject standard SETTINGS IDs in
customSettings.These entries are documented as non-standard, but IDs like
2are accepted and serialized asSETTINGS_ENABLE_PUSH; values such as{ customSettings: { 2: 2 } }put an invalid standard setting on the wire and trigger peer protocol errors. Reject IDs already handled by the standard SETTINGS fields.Proposed validation guard
- if setting_id > 0xFFFF { + if setting_id > 0xFFFF + || matches!(setting_id, 0x1 | 0x2 | 0x3 | 0x4 | 0x5 | 0x6 | 0x8) + { return global_object .err_http2_invalid_setting_value_range_error( "Invalid custom setting identifier",🤖 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/runtime/api/bun/h2_frame_parser.rs` around lines 6237 - 6269, After the existing range validation that checks if setting_id > 0xFFFF, add an additional validation to reject standard HTTP/2 SETTINGS IDs (IDs 1-6) that should not be allowed in customSettings. Insert a check that returns an error if the setting_id falls within the standard range of reserved SETTINGS identifiers, preventing invalid standard settings from being added to the custom_settings collection via the with_mut call.src/js/node/http2.ts (2)
2947-2955: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winMake fd ownership per operation, not stream-global.
this[kOwnsFd]is sticky afterrespondWithFile(). If that call fails andonErrorrecovers by callingrespondWithFD(), the caller-owned fd is treated as owned here and can be closed viatryClose(),autoClose, or the stream close handler. Pass ownership intodoSendFileFD()/afterOpen()instead of storing it on the stream.Proposed direction
-function doSendFileFD(options, fd, headers, err, stat) { +function doSendFileFD(ownsFd, options, fd, headers, err, stat) { const onError = options.onError; - const ownsFd = this[kOwnsFd] === true; if (err) { if (ownsFd && err.code !== "EBADF") { tryClose(fd); @@ - if (this[kOwnsFd] === true) tryClose(fd); + if (ownsFd) tryClose(fd); @@ - if (this[kOwnsFd] === true) tryClose(fd); + if (ownsFd) tryClose(fd); @@ - autoClose: this[kOwnsFd] === true, + autoClose: ownsFd, @@ -function afterOpen(options, headers, err, fd) { +function afterOpen(options, headers, err, fd) { @@ - fs.fstat(fd, doSendFileFD.bind(this, options, fd, headers)); + fs.fstat(fd, doSendFileFD.bind(this, true, options, fd, headers)); } @@ - this[kOwnsFd] = true; fs.open(path, "r", afterOpen.bind(this, options || {}, headers)); @@ - fs.fstat(fd.fd, doSendFileFD.bind(this, options, fd, headers)); + fs.fstat(fd.fd, doSendFileFD.bind(this, false, options, fd, headers)); } else { - fs.fstat(fd, doSendFileFD.bind(this, options, fd, headers)); + fs.fstat(fd, doSendFileFD.bind(this, false, options, fd, headers)); }Also applies to: 3035-3058, 3083-3086, 3285-3286, 3344-3354
🤖 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/js/node/http2.ts` around lines 2947 - 2955, The fd ownership is currently stored as a sticky property on the stream object via this[kOwnsFd], which persists incorrectly across multiple file operations. Instead of relying on this stream-level property, refactor the code to pass the ownership information as a parameter into the doSendFileFD() and afterOpen() functions. This way, each operation tracks whether it owns the fd independently, preventing the caller-owned fd from being incorrectly closed when a previous respondWithFile() call failed and onError recovery attempts to use respondWithFD() with a caller-provided fd.Source: Coding guidelines
5411-5479: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDestroy open client streams synchronously during session destroy.
The server path uses
destroyStreamForSessionDestroy()beforeemitErrorToAllStreams(), matching the helper’s invariant that writes immediately aftersession.destroy()observe destroyed streams. The client path only emits parser errors, whose handlers defer teardown, so open client streams can remain writable/readable for a tick afterClientHttp2Session.destroy()returns.Proposed fix
if (parser) { // node cancels streams still open when their session is destroyed: each gets // ERR_HTTP2_STREAM_CANCEL (or the session error when one was provided), with the CANCEL // rst code. if (this[kSessionDestroyError] == null && error == null) { this[kSessionDestroyError] = createPendingStreamCancelError(); } // Like Node's Http2Stream._destroy: a received GOAWAY's code takes // precedence over the destroy code when streams are torn down. - parser.emitErrorToAllStreams(this[kGoawayCode] || (code !== undefined ? code : constants.NGHTTP2_CANCEL)); + const streamRstCode = this[kGoawayCode] || (code !== undefined ? code : constants.NGHTTP2_CANCEL); + parser.forEachStream( + FunctionPrototypeBind.$call( + destroyStreamForSessionDestroy, + undefined, + this[kSessionDestroyError] || error, + streamRstCode, + ), + ); + parser.emitErrorToAllStreams(streamRstCode); parser.detach(); }🤖 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/js/node/http2.ts` around lines 5411 - 5479, The destroy() method only emits parser errors to streams without synchronously destroying them first, allowing open streams to remain writable/readable for a tick after destroy() returns. Before calling parser.emitErrorToAllStreams() on the parser object, add a call to destroyStreamForSessionDestroy() to synchronously destroy all open streams immediately, matching the server path behavior and ensuring streams are torn down during the destroy() call rather than being deferred.
♻️ Duplicate comments (1)
src/js/node/_http_server.ts (1)
1172-1194: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRequire the full HTTP/2 preface before reporting
HPE_PAUSED_H2_UPGRADE.Line 1175 checks only the first 16 bytes, but Line 1193 reports the 24-byte HTTP/2 preface as parsed. A partial
PRI * HTTP/2.0\r\npacket is misclassified as an HTTP/2 upgrade pause.Proposed fix
-const kHttp2PrefaceStart = [ +const kHttp2Preface = [ 0x50, 0x52, 0x49, 0x20, 0x2a, 0x20, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x32, 0x2e, 0x30, 0x0d, 0x0a, -]; // "PRI * HTTP/2.0\r\n" + 0x0d, 0x0a, 0x53, 0x4d, 0x0d, 0x0a, 0x0d, 0x0a, +]; // "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" function isHttp2Preface(rawPacket: ArrayBuffer) { - if (rawPacket.byteLength < kHttp2PrefaceStart.length) return false; - const bytes = new Uint8Array(rawPacket, 0, kHttp2PrefaceStart.length); - for (let i = 0; i < kHttp2PrefaceStart.length; i++) { - if (bytes[i] !== kHttp2PrefaceStart[i]) return false; + if (rawPacket.byteLength < kHttp2Preface.length) return false; + const bytes = new Uint8Array(rawPacket, 0, kHttp2Preface.length); + for (let i = 0; i < kHttp2Preface.length; i++) { + if (bytes[i] !== kHttp2Preface[i]) return false; } return true; }- err.bytesParsed = 24; + err.bytesParsed = kHttp2Preface.length;🤖 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/js/node/_http_server.ts` around lines 1172 - 1194, The kHttp2PrefaceStart array currently contains only the first 16 bytes of the HTTP/2 connection preface, but the error handler in onServerClientError reports 24 bytes as parsed when HPE_PAUSED_H2_UPGRADE is detected. Update the kHttp2PrefaceStart array to include all 24 bytes of the complete HTTP/2 preface (the additional 8 bytes after "PRI * HTTP/2.0\r\n") so that the isHttp2Preface function only returns true when the full preface is present, preventing partial packets from being misclassified as HTTP/2 upgrade attempts.
🤖 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/tls.ts`:
- Around line 126-128: The passphrase fallback logic in the per-entry pfx
handling is using strict undefined checking instead of falsy-fallback semantics,
which diverges from Node.js behavior. At the point where entryPassphrase is
checked (line 127), change the condition from checking if entryPassphrase is not
undefined to instead using a truthy check, so that falsy values like empty
strings, null, or false do not override the top-level passphrase and instead
fall back to it, matching Node.js v26.3.0 compatibility.
In `@src/js/node/_http_client.ts`:
- Around line 783-790: In the domain binding logic where reqDomain is checked
for the add function, the order of operations needs to be reversed. Currently,
res.domain is being assigned to reqDomain before calling reqDomain.add(res), but
this should be done in the opposite order. Move the reqDomain.add(res) call to
execute before the res.domain assignment to ensure that domain implementations
can properly register the response emitter without the add method returning
early.
In `@src/js/node/_http_server.ts`:
- Around line 1008-1023: The pipeline advancement calls at lines following the
isPipelined check, within the finish event listener setup, and at other
locations (around line 2228) are advancing the pipeline unconditionally even
when the response requires closing the connection. To fix this, add a condition
before each advanceResponsePipeline call to check if the connection should be
closed (e.g., by examining the Connection header, HTTP version compatibility, or
close-delimited status), and only call advanceResponsePipeline if the connection
is not required to close. Apply this check to all three locations mentioned: the
direct advanceResponsePipeline call after the handle.finished check, the
advanceResponsePipeline binding in the finish event listener, and the similar
calls around line 2228.
- Around line 2412-2416: Queued pipelined responses need to buffer informational
and flush operations instead of throwing or silently dropping them. Modify the
methods writeEarlyHints(), writeProcessing(), writeInformation(),
writeContinue(), and flushHeaders() to check if kPipelinedQueuedState is defined
and queue these operations (similar to how write/end operations are queued)
rather than attempting to call this.socket.write() which will fail on null
socket. Store the queued operations in a buffer and replay them after the socket
is assigned to the response during socket assignment.
- Around line 416-422: The issue is in the setupConnectionsTracking function
where the delay assignment uses the OR operator (||) which treats 0 as falsy and
defaults to 30_000, even though 0 is a valid value for
connectionsCheckingInterval. Replace the || operator with the nullish coalescing
operator (??) when assigning the delay constant so that only null or undefined
values trigger the fallback to 30_000, while explicitly set 0 values are
preserved as distinct from unset values.
In `@src/js/node/https.ts`:
- Around line 503-509: The createServer function spreads the options parameter
without validating it first, causing primitive inputs to be coerced into objects
instead of being rejected. Import validateObject from internal/validators at the
top of the file if not already present, then add a call to
validateObject(options, "options") immediately after the conditional check that
handles the function-as-first-argument case and before any object spread
operation. This ensures options is validated as an object before spreading,
matching Node.js behavior and preventing silent coercion of invalid inputs like
strings or numbers.
In `@src/runtime/api/bun/h2_frame_parser.rs`:
- Around line 6089-6092: The issue is that explicit_settings and custom_settings
are using persistent parser state that accumulates across multiple calls,
causing re-emission of previously sent settings even when omitted in the current
submission. Instead of using self.explicit_settings and self.custom_settings
directly in the number_setting! macro and when calling set_settings() and
write_settings_payload(), create local per-submission variables (a new
explicit_settings mask and custom_settings vector) that are initialized fresh
for each SETTINGS frame being parsed. Populate these local variables during the
current submission's parsing, then pass these local per-submission variables to
set_settings() and write_settings_payload() calls to ensure only the current
submission's settings are serialized, rather than the cumulative state.
In `@src/runtime/api/bun/h2/connection.rs`:
- Around line 1103-1112: The invalid frame budget comparison is checking the old
count value before incrementing, which causes the limit to trigger one frame
late. In the code block where self.invalid_frame_count is incremented (around
the saturating_add call), you must compare the new incremented value against
self.max_invalid_frames instead of comparing the original count variable. This
same off-by-one issue also appears in another location in the file around lines
1293-1298, so apply the same fix to both occurrences to ensure the budget is
enforced correctly from the first invalid frame.
- Around line 20-23: The issue is that when acknowledging SETTINGS, the code
pops the oldest submission from pending_local_settings_acks but then passes
self.local_settings to on_local_settings(), which can cause a later
unacknowledged submission to be incorrectly reported as acknowledged. Capture
the full Settings snapshot that was popped from pending_local_settings_acks
queue and pass that specific snapshot to the on_local_settings() method (which
appears to be called around lines 300-304 and 628-640) instead of passing
self.local_settings, ensuring the sink is notified with the exact acknowledged
SETTINGS snapshot.
- Around line 341-345: The `send_go_away()` method accepts a `code: ErrorCode`
parameter but never uses it, always hardcoding `wire::lib_error::PROTO` instead
when calling `local_connection_error()`. Since this is a public method that is
currently uncalled, fix the API contract by either removing the unused `code`
parameter from the function signature (and update the documentation comment to
clarify it only sends error GOAWAY frames with PROTO code), or update the
implementation to use the provided `code` parameter and pass it to
`local_connection_error()` instead of the hardcoded PROTO value. Choose the
approach that best fits your intended semantics for graceful versus error GOAWAY
frames.
In `@test/js/node/http2/h2-conformance.test.ts`:
- Line 558: Remove the dynamic require() statement for Writable from line 558
and add it to the module-scope imports at the top of the file alongside other
dependencies like http2 and net. Import Writable directly from node:stream at
the beginning of the file using the standard import pattern, maintaining any
necessary TypeScript type assertions for consistency with the existing import
style in this test file.
In `@test/js/node/test/common/index.js`:
- Around line 1319-1325: The getOptionValue method has a hardcoded return false
for the "--insecure-http-parser" case, but it should instead query the actual
flag state from process.execArgv to reflect flags parsed by parseTestFlags.
Replace the hardcoded false return with a check that determines whether
"--insecure-http-parser" exists in process.execArgv and returns true if present
or false if absent, ensuring tests that set this flag receive the correct value.
---
Outside diff comments:
In `@src/js/node/http2.ts`:
- Around line 2947-2955: The fd ownership is currently stored as a sticky
property on the stream object via this[kOwnsFd], which persists incorrectly
across multiple file operations. Instead of relying on this stream-level
property, refactor the code to pass the ownership information as a parameter
into the doSendFileFD() and afterOpen() functions. This way, each operation
tracks whether it owns the fd independently, preventing the caller-owned fd from
being incorrectly closed when a previous respondWithFile() call failed and
onError recovery attempts to use respondWithFD() with a caller-provided fd.
- Around line 5411-5479: The destroy() method only emits parser errors to
streams without synchronously destroying them first, allowing open streams to
remain writable/readable for a tick after destroy() returns. Before calling
parser.emitErrorToAllStreams() on the parser object, add a call to
destroyStreamForSessionDestroy() to synchronously destroy all open streams
immediately, matching the server path behavior and ensuring streams are torn
down during the destroy() call rather than being deferred.
In `@src/runtime/api/bun/h2_frame_parser.rs`:
- Around line 6237-6269: After the existing range validation that checks if
setting_id > 0xFFFF, add an additional validation to reject standard HTTP/2
SETTINGS IDs (IDs 1-6) that should not be allowed in customSettings. Insert a
check that returns an error if the setting_id falls within the standard range of
reserved SETTINGS identifiers, preventing invalid standard settings from being
added to the custom_settings collection via the with_mut call.
---
Duplicate comments:
In `@src/js/node/_http_server.ts`:
- Around line 1172-1194: The kHttp2PrefaceStart array currently contains only
the first 16 bytes of the HTTP/2 connection preface, but the error handler in
onServerClientError reports 24 bytes as parsed when HPE_PAUSED_H2_UPGRADE is
detected. Update the kHttp2PrefaceStart array to include all 24 bytes of the
complete HTTP/2 preface (the additional 8 bytes after "PRI * HTTP/2.0\r\n") so
that the isHttp2Preface function only returns true when the full preface is
present, preventing partial packets from being misclassified as HTTP/2 upgrade
attempts.
🪄 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: fc379c9a-b81c-4cae-9235-55da0f8846c9
📒 Files selected for processing (237)
packages/bun-usockets/src/context.cpackages/bun-usockets/src/crypto/openssl.cpackages/bun-usockets/src/internal/internal.hpackages/bun-usockets/src/libusockets.hpackages/bun-uws/src/App.hpackages/bun-uws/src/ChunkedEncoding.hpackages/bun-uws/src/HttpContext.hpackages/bun-uws/src/HttpContextData.hpackages/bun-uws/src/HttpErrors.hpackages/bun-uws/src/HttpParser.hpackages/bun-uws/src/HttpResponse.hpackages/bun-uws/src/HttpResponseData.hsrc/js/internal/http.tssrc/js/internal/timers.tssrc/js/internal/tls.tssrc/js/node/_http_client.tssrc/js/node/_http_common.tssrc/js/node/_http_incoming.tssrc/js/node/_http_outgoing.tssrc/js/node/_http_server.tssrc/js/node/domain.tssrc/js/node/http2.tssrc/js/node/https.tssrc/js/node/net.tssrc/js/node/tls.tssrc/jsc/ErrorCode.rssrc/jsc/bindings/ErrorCode.tssrc/jsc/bindings/NodeHTTP.cppsrc/jsc/bindings/node/JSNodeHTTPServerSocket.cppsrc/jsc/bindings/node/JSNodeHTTPServerSocket.hsrc/jsc/bindings/node/JSNodeHTTPServerSocketPrototype.cppsrc/runtime/api/bun/h2/connection.rssrc/runtime/api/bun/h2/wire.rssrc/runtime/api/bun/h2_frame_parser.rssrc/runtime/api/h2.classes.tssrc/runtime/server/mod.rssrc/runtime/server/server_body.rssrc/uws_sys/App.rssrc/uws_sys/libuwsockets.cpptest/expectations.txttest/js/bun/test/parallel/test-http-host-array-should-throw-in-request.tstest/js/bun/test/parallel/test-http-should-emit-timeout-event-when-using-server-setTimeout.tstest/js/bun/test/parallel/test-http-should-emit-timeout-event.tstest/js/node/http/node-http.test.tstest/js/node/http2/h2-conformance.test.tstest/js/node/http2/node-http2.test.jstest/js/node/net/node-net.test.tstest/js/node/test/common/index.jstest/js/node/test/parallel/test-http-abort-stream-end.jstest/js/node/test/parallel/test-http-agent-domain-reused-gc.jstest/js/node/test/parallel/test-http-agent-keepalive-delay.jstest/js/node/test/parallel/test-http-agent-maxtotalsockets.jstest/js/node/test/parallel/test-http-agent-remove.jstest/js/node/test/parallel/test-http-allow-content-length-304.jstest/js/node/test/parallel/test-http-autoselectfamily.jstest/js/node/test/parallel/test-http-buffer-sanity.jstest/js/node/test/parallel/test-http-chunk-extensions-limit.jstest/js/node/test/parallel/test-http-chunk-problem.jstest/js/node/test/parallel/test-http-client-abort-keep-alive-queued-unix-socket.jstest/js/node/test/parallel/test-http-client-abort-unix-socket.jstest/js/node/test/parallel/test-http-client-close-with-default-agent.jstest/js/node/test/parallel/test-http-client-finished.jstest/js/node/test/parallel/test-http-client-immediate-error.jstest/js/node/test/parallel/test-http-client-keep-alive-hint.jstest/js/node/test/parallel/test-http-client-pipe-end.jstest/js/node/test/parallel/test-http-client-reject-unexpected-agent.jstest/js/node/test/parallel/test-http-client-request-options.jstest/js/node/test/parallel/test-http-client-response-domain.jstest/js/node/test/parallel/test-http-client-response-timeout.jstest/js/node/test/parallel/test-http-client-spurious-aborted.jstest/js/node/test/parallel/test-http-client-timeout-event.jstest/js/node/test/parallel/test-http-client-timeout-on-connect.jstest/js/node/test/parallel/test-http-client-timeout-option.jstest/js/node/test/parallel/test-http-client-timeout.jstest/js/node/test/parallel/test-http-client-with-create-connection.jstest/js/node/test/parallel/test-http-connect-req-res.jstest/js/node/test/parallel/test-http-connect.jstest/js/node/test/parallel/test-http-content-length-mismatch.jstest/js/node/test/parallel/test-http-correct-hostname.jstest/js/node/test/parallel/test-http-date-header.jstest/js/node/test/parallel/test-http-decoded-auth.jstest/js/node/test/parallel/test-http-double-content-length.jstest/js/node/test/parallel/test-http-dump-req-when-res-ends.jstest/js/node/test/parallel/test-http-early-hints-invalid-argument.jstest/js/node/test/parallel/test-http-end-throw-socket-handling.jstest/js/node/test/parallel/test-http-expect-handling.jstest/js/node/test/parallel/test-http-extra-response.jstest/js/node/test/parallel/test-http-flush-headers.jstest/js/node/test/parallel/test-http-flush-response-headers.jstest/js/node/test/parallel/test-http-generic-streams.jstest/js/node/test/parallel/test-http-head-throw-on-response-body-write.jstest/js/node/test/parallel/test-http-header-badrequest.jstest/js/node/test/parallel/test-http-header-obstext.jstest/js/node/test/parallel/test-http-header-read.jstest/js/node/test/parallel/test-http-header-value-relaxed.jstest/js/node/test/parallel/test-http-highwatermark.jstest/js/node/test/parallel/test-http-host-headers.jstest/js/node/test/parallel/test-http-hostname-typechecking.jstest/js/node/test/parallel/test-http-incoming-pipelined-socket-destroy.jstest/js/node/test/parallel/test-http-insecure-parser-per-stream.jstest/js/node/test/parallel/test-http-insecure-parser.jstest/js/node/test/parallel/test-http-invalidheaderfield.jstest/js/node/test/parallel/test-http-invalidheaderfield2.jstest/js/node/test/parallel/test-http-keep-alive-drop-requests.jstest/js/node/test/parallel/test-http-keep-alive-empty-line.mjstest/js/node/test/parallel/test-http-keep-alive-max-requests.jstest/js/node/test/parallel/test-http-localaddress.jstest/js/node/test/parallel/test-http-many-ended-pipelines.jstest/js/node/test/parallel/test-http-max-header-size-per-stream.jstest/js/node/test/parallel/test-http-max-http-headers.jstest/js/node/test/parallel/test-http-multiple-headers.jstest/js/node/test/parallel/test-http-no-read-no-dump.jstest/js/node/test/parallel/test-http-outgoing-drain-writable-length.jstest/js/node/test/parallel/test-http-outgoing-finished.jstest/js/node/test/parallel/test-http-outgoing-proto.jstest/js/node/test/parallel/test-http-outgoing-renderHeaders.jstest/js/node/test/parallel/test-http-parser-finish-error.jstest/js/node/test/parallel/test-http-parser-free.jstest/js/node/test/parallel/test-http-parser-freed-before-upgrade.jstest/js/node/test/parallel/test-http-parser-freed-during-execute.jstest/js/node/test/parallel/test-http-parser-memory-retention.jstest/js/node/test/parallel/test-http-parser-multiple-execute.jstest/js/node/test/parallel/test-http-parser-timeout-reset.jstest/js/node/test/parallel/test-http-parser.jstest/js/node/test/parallel/test-http-pause.jstest/js/node/test/parallel/test-http-pipeline-assertionerror-finish.jstest/js/node/test/parallel/test-http-pipeline-flood.jstest/js/node/test/parallel/test-http-pipeline-outgoing-destroy.jstest/js/node/test/parallel/test-http-proxy.jstest/js/node/test/parallel/test-http-raw-headers.jstest/js/node/test/parallel/test-http-readable-data-event.jstest/js/node/test/parallel/test-http-req-close-robust-from-tampering.jstest/js/node/test/parallel/test-http-req-res-close.jstest/js/node/test/parallel/test-http-request-end-twice.jstest/js/node/test/parallel/test-http-request-end.jstest/js/node/test/parallel/test-http-request-method-delete-payload.jstest/js/node/test/parallel/test-http-response-add-header-after-sent.jstest/js/node/test/parallel/test-http-response-readable.jstest/js/node/test/parallel/test-http-response-remove-header-after-sent.jstest/js/node/test/parallel/test-http-response-setheaders.jstest/js/node/test/parallel/test-http-response-status-message.jstest/js/node/test/parallel/test-http-response-statuscode.jstest/js/node/test/parallel/test-http-response-writehead-returns-this.jstest/js/node/test/parallel/test-http-same-map.jstest/js/node/test/parallel/test-http-server-client-error.jstest/js/node/test/parallel/test-http-server-close-all.jstest/js/node/test/parallel/test-http-server-close-idle-wait-response.jstest/js/node/test/parallel/test-http-server-close-idle.jstest/js/node/test/parallel/test-http-server-connection-list-when-close.jstest/js/node/test/parallel/test-http-server-destroy-socket-on-client-error.jstest/js/node/test/parallel/test-http-server-drop-connections-in-cluster.jstest/js/node/test/parallel/test-http-server-headers-timeout-delayed-headers.jstest/js/node/test/parallel/test-http-server-headers-timeout-interrupted-headers.jstest/js/node/test/parallel/test-http-server-headers-timeout-keepalive.jstest/js/node/test/parallel/test-http-server-headers-timeout-pipelining.jstest/js/node/test/parallel/test-http-server-keep-alive-timeout.jstest/js/node/test/parallel/test-http-server-keepalive-end.jstest/js/node/test/parallel/test-http-server-method.query.jstest/js/node/test/parallel/test-http-server-multiheaders.jstest/js/node/test/parallel/test-http-server-multiple-client-error.jstest/js/node/test/parallel/test-http-server-non-utf8-header.jstest/js/node/test/parallel/test-http-server-options-highwatermark.jstest/js/node/test/parallel/test-http-server-options-incoming-message.jstest/js/node/test/parallel/test-http-server-options-server-response.jstest/js/node/test/parallel/test-http-server-reject-chunked-with-content-length.jstest/js/node/test/parallel/test-http-server-reject-cr-no-lf.jstest/js/node/test/parallel/test-http-server-request-timeout-delayed-body.jstest/js/node/test/parallel/test-http-server-request-timeout-delayed-headers.jstest/js/node/test/parallel/test-http-server-request-timeout-interrupted-body.jstest/js/node/test/parallel/test-http-server-request-timeout-interrupted-headers.jstest/js/node/test/parallel/test-http-server-request-timeout-keepalive.jstest/js/node/test/parallel/test-http-server-request-timeout-pipelining.jstest/js/node/test/parallel/test-http-server-request-timeout-upgrade.jstest/js/node/test/parallel/test-http-server-stale-close.jstest/js/node/test/parallel/test-http-server-unconsume.jstest/js/node/test/parallel/test-http-server.jstest/js/node/test/parallel/test-http-set-cookies.jstest/js/node/test/parallel/test-http-set-header-chain.jstest/js/node/test/parallel/test-http-set-timeout-server.jstest/js/node/test/parallel/test-http-set-timeout.jstest/js/node/test/parallel/test-http-set-trailers.jstest/js/node/test/parallel/test-http-socket-encoding-error.jstest/js/node/test/parallel/test-http-status-code.jstest/js/node/test/parallel/test-http-status-message.jstest/js/node/test/parallel/test-http-timeout-overflow.jstest/js/node/test/parallel/test-http-transfer-encoding-repeated-chunked.jstest/js/node/test/parallel/test-http-unix-socket.jstest/js/node/test/parallel/test-http-upgrade-server-callback.jstest/js/node/test/parallel/test-http-upgrade-server-with-body-and-extras.mjstest/js/node/test/parallel/test-http-upgrade-server-with-body-error.mjstest/js/node/test/parallel/test-http-upgrade-server-with-body.mjstest/js/node/test/parallel/test-http-upgrade-server-with-large-body-unread.mjstest/js/node/test/parallel/test-http-upgrade-server-with-large-body.mjstest/js/node/test/parallel/test-http-url.parse-basic.jstest/js/node/test/parallel/test-http-url.parse-https.request.jstest/js/node/test/parallel/test-http-write-callbacks.jstest/js/node/test/parallel/test-http-zero-length-write.jstest/js/node/test/parallel/test-https-agent-additional-options.jstest/js/node/test/parallel/test-https-agent-keylog.jstest/js/node/test/parallel/test-https-agent-session-eviction.jstest/js/node/test/parallel/test-https-agent-sni.jstest/js/node/test/parallel/test-https-agent.jstest/js/node/test/parallel/test-https-argument-of-creating.jstest/js/node/test/parallel/test-https-autoselectfamily.jstest/js/node/test/parallel/test-https-byteswritten.jstest/js/node/test/parallel/test-https-client-renegotiation-limit.jstest/js/node/test/parallel/test-https-insecure-parse-per-stream.jstest/js/node/test/parallel/test-https-keep-alive-drop-requests.jstest/js/node/test/parallel/test-https-localaddress.jstest/js/node/test/parallel/test-https-max-header-size-per-stream.jstest/js/node/test/parallel/test-https-max-headers-count.jstest/js/node/test/parallel/test-https-options-boolean-check.jstest/js/node/test/parallel/test-https-pfx.jstest/js/node/test/parallel/test-https-resume-after-renew.jstest/js/node/test/parallel/test-https-selfsigned-no-keycertsign-no-crash.jstest/js/node/test/parallel/test-https-server-close-all.jstest/js/node/test/parallel/test-https-server-close-idle.jstest/js/node/test/parallel/test-https-set-timeout-server.jstest/js/node/test/parallel/test-https-strict.jstest/js/node/test/parallel/test-https-timeout-server-2.jstest/js/node/test/parallel/test-https-timeout-server.jstest/js/node/test/parallel/test-https-unix-socket-self-signed.jstest/js/node/test/parallel/test-tls-options-boolean-check.jstest/js/node/test/sequential/test-http-econnrefused.jstest/js/node/test/sequential/test-http-keep-alive-large-write.jstest/js/node/test/sequential/test-http-regr-gh-2928.jstest/js/node/test/sequential/test-http-server-keep-alive-timeout-slow-client-headers.jstest/js/node/test/sequential/test-http-server-keep-alive-timeout-slow-server.jstest/js/node/test/sequential/test-http-server-request-timeouts-mixed.jstest/js/node/test/sequential/test-http2-max-session-memory.jstest/js/node/test/sequential/test-http2-ping-flood.jstest/js/node/test/sequential/test-http2-settings-flood.jstest/js/node/test/sequential/test-http2-timeout-large-write-file.jstest/js/node/test/sequential/test-http2-timeout-large-write.jstest/js/node/test/sequential/test-https-connect-localport.jstest/js/node/test/sequential/test-https-server-keep-alive-timeout.jstest/regression/issue/25190.test.ts
💤 Files with no reviewable changes (5)
- test/js/node/test/parallel/test-http-client-with-create-connection.js
- test/js/node/test/parallel/test-http-client-response-timeout.js
- test/js/node/test/parallel/test-https-unix-socket-self-signed.js
- test/js/node/test/parallel/test-http-unix-socket.js
- test/js/node/test/parallel/test-http-client-pipe-end.js
038565b to
6a5afa4
Compare
There was a problem hiding this comment.
Actionable comments posted: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
src/runtime/api/bun/h2_frame_parser.rs (1)
6242-6274: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject standard SETTINGS IDs in
customSettings.Line 6255 only checks
<= 0xffff, socustomSettings: { "4": 1 }emitsSETTINGS_INITIAL_WINDOW_SIZEwithout updatinglocal_settingsor the pending ACK metadata. That can desynchronize Bun’s flow-control/header/frame-size state from what was actually sent on the wire. Also reject non-finite or fractional values before theas u32cast.As per coding guidelines, “Validate untrusted input BEFORE any processing, allocation, or side effect.”
Suggested fix
if setting_id > 0xFFFF { return global_object .err_http2_invalid_setting_value_range_error( "Invalid custom setting identifier", ) .throw(); } + if matches!(setting_id, 0x1 | 0x2 | 0x3 | 0x4 | 0x5 | 0x6 | 0x8) { + return global_object + .err_http2_invalid_setting_value_range_error( + "Invalid custom setting identifier", + ) + .throw(); + } // Validate setting value is in range [0, 2^32-1] let setting_value = iter.value; if setting_value.is_number() { let value = setting_value.as_number(); - if value < 0.0 || value > MAX_HEADER_TABLE_SIZE_F64 { + if !value.is_finite() + || value.fract() != 0.0 + || value < 0.0 + || value > MAX_HEADER_TABLE_SIZE_F64 + { return global_object .err_http2_invalid_setting_value_range_error( "Invalid custom setting value", ) .throw(); }🤖 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/runtime/api/bun/h2_frame_parser.rs` around lines 6242 - 6274, The customSettings validation is incomplete and allows standard HTTP/2 SETTINGS IDs (0-5) to be passed through, which can desynchronize Bun's internal state. After parsing setting_id and validating it is within [0, 0xFFFF], add an additional check to reject any setting_id values that are in the reserved standard range (0-5). Additionally, before casting setting_value to u32, add validation to reject non-finite values (check for NaN and Infinity using appropriate float methods) and reject fractional values (check if the value differs from its truncated integer form) to prevent silent data loss during the as u32 cast in the staged_custom.push call.Source: Coding guidelines
src/js/node/http2.ts (3)
3020-3058: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winHandle zero-length file ranges before creating the read stream.
For an empty file, or
offsetat/past EOF,statOptions.lengthcan become0or negative, so Line 3057 computesendasoffset - 1after already settingContent-Length. That turns a valid empty response into a read-stream range error or an invalid negative length. Clamp remaining bytes to0and finish the native stream without creating a file stream when there is no body.Proposed fix
if (stat.isFile()) { + const remaining = Math.max(0, stat.size - +statOptions.offset); statOptions.length = statOptions.length < 0 - ? stat.size - +statOptions.offset - : Math.min(stat.size - +statOptions.offset, statOptions.length); + ? remaining + : Math.min(remaining, statOptions.length); @@ const finishNativeStream = closeWritableForFileResponse(this); + + if (statOptions.length === 0) { + if (ownsFd) tryClose(fd); + finishNativeStream(() => {}); + return; + } const stream = this; const fileStream = fs.createReadStream(null, { @@ - end: typeof statOptions.length === "number" ? statOptions.length + (statOptions.offset || 0) - 1 : undefined, + end: typeof statOptions.length === "number" ? statOptions.length + (statOptions.offset || 0) - 1 : undefined,🤖 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/js/node/http2.ts` around lines 3020 - 3058, The issue is that when statOptions.length becomes zero or negative (for empty files or when offset exceeds file size), the code still creates a read stream with an invalid end value calculated on line 3057, causing range errors. After computing statOptions.length on lines 3020-3023, add a check: if statOptions.length is less than or equal to 0, call the finishNativeStream() function to properly end the response without creating the fileStream, then return early. This ensures zero-length responses are handled correctly without attempting to create invalid read stream ranges.
5469-5480: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winDestroy open client streams synchronously before detaching the parser.
ServerHttp2Session.destroy()usesdestroyStreamForSessionDestroy()beforeemitErrorToAllStreams(), but the client path only emits native errors and detaches. Open client streams can remain writable until the async native teardown runs, so a write immediately aftersession.destroy()can observe a live stream instead of Node’s synchronous cancelled/destroyed state.Proposed fix
const parser = this.#parser; if (parser) { @@ - parser.emitErrorToAllStreams(this[kGoawayCode] || (code !== undefined ? code : constants.NGHTTP2_CANCEL)); + const streamRstCode = this[kGoawayCode] || (code !== undefined ? code : constants.NGHTTP2_CANCEL); + const streamDestroyError = this[kSessionDestroyError] ?? error; + parser.forEachStream( + FunctionPrototypeBind.$call(destroyStreamForSessionDestroy, undefined, streamDestroyError, streamRstCode), + ); + parser.emitErrorToAllStreams(streamRstCode); parser.detach(); }🤖 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/js/node/http2.ts` around lines 5469 - 5480, The client stream destruction path is not synchronously destroying open streams before detaching the parser, unlike the ServerHttp2Session.destroy() implementation which calls destroyStreamForSessionDestroy() before emitErrorToAllStreams(). Add a synchronous destruction of all open client streams using destroyStreamForSessionDestroy() before the parser.emitErrorToAllStreams() call to ensure streams transition to a cancelled/destroyed state immediately rather than remaining writable during asynchronous native teardown.
2947-3058: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winKeep fd ownership per response operation, not on the stream.
Line 3285 leaves
kOwnsFdset on the stream. IfrespondWithFile()fails orstatCheckcancels and user code falls back torespondWithFD()on the same stream, the caller-owned fd is treated as owned and can be closed by Line 3055 or the error paths. Pass ownership through the async callback chain instead of storing stale mutable state on the stream.Proposed fix
-function doSendFileFD(options, fd, headers, err, stat) { +function doSendFileFD(ownsFd, options, fd, headers, err, stat) { const onError = options.onError; - const ownsFd = this[kOwnsFd] === true; @@ - if (this[kOwnsFd] === true) tryClose(fd); + if (ownsFd) tryClose(fd); @@ - if (this[kOwnsFd] === true) tryClose(fd); + if (ownsFd) tryClose(fd); @@ - autoClose: this[kOwnsFd] === true, + autoClose: ownsFd,- fs.fstat(fd, doSendFileFD.bind(this, options, fd, headers)); + fs.fstat(fd, doSendFileFD.bind(this, true, options, fd, headers));- this[kOwnsFd] = true; fs.open(path, "r", afterOpen.bind(this, options || {}, headers));- fs.fstat(fd.fd, doSendFileFD.bind(this, options, fd, headers)); + fs.fstat(fd.fd, doSendFileFD.bind(this, false, fd, headers)); } else { - fs.fstat(fd, doSendFileFD.bind(this, options, fd, headers)); + fs.fstat(fd, doSendFileFD.bind(this, false, fd, headers)); }Also applies to: 3285-3354
🤖 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/js/node/http2.ts` around lines 2947 - 3058, The `kOwnsFd` flag is being stored as persistent mutable state on the stream, which causes problems when multiple response operations occur on the same stream (e.g., if `respondWithFile()` fails and user code falls back to `respondWithFD()`). Instead of checking `this[kOwnsFd]` in the `doSendFileFD` function and relying on stream-level state, pass the fd ownership information through the async callback chain as a parameter. This ensures each response operation independently tracks whether it owns the fd, rather than inheriting stale ownership state from previous failed operations. Remove the stream-level `kOwnsFd` assignment and refactor the callback signatures to carry the ownership flag through the operation sequence.
🤖 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 `@packages/bun-uws/src/HttpParser.h`:
- Around line 1105-1114: The chunk-extension overflow validation in the HTTP
parser is currently performed after the dataHandler is called, allowing
oversized chunk extensions to be processed and delivered to user code before
rejection. Move the check for chunkedExtensionsByteCount >
MAX_CHUNK_EXTENSION_SIZE to occur before the dataHandler call in the
ChunkIterator loops, so untrusted input is validated before side effects. Apply
this change to all three affected loop locations (around lines 1105, 1179, and
1248) while keeping the existing post-loop overflow guard for fragmented
extension lines that don't yield a complete chunk.
- Line 981: The current comparison using std::max<uint64_t>(MAX_FALLBACK_SIZE,
maxHeaderSize) enforces MAX_FALLBACK_SIZE as a floor value, which prevents
respecting smaller maxHeaderSize configurations. Replace this comparison logic
at line 981 and the additional occurrences around lines 1165-1168 to treat
maxHeaderSize of 0 as the default (using MAX_FALLBACK_SIZE), but otherwise honor
the configured maxHeaderSize cap. Use a conditional expression (ternary
operator) to check if maxHeaderSize is non-zero and use it directly, or fall
back to MAX_FALLBACK_SIZE only when maxHeaderSize equals 0, ensuring
user-configured resource limits are actually enforced rather than silently
overridden by the larger default.
In `@src/js/internal/http.ts`:
- Around line 194-199: The trailer handling code at the EOF path is accessing
the socket handle through the public user-visible self.socket property, which
can be replaced or intercepted with a getter that throws or skips trailers.
Instead of reading socketHandle from self.socket?.[kHandle], capture and store
the socket handle as a private field at object construction time (before the
request is exposed to user code), or access it directly via a native binding.
Update the code to use the privately stored socket handle rather than
dereferencing through the public socket property to ensure internal logic cannot
be subverted by user code overriding the socket property.
In `@src/js/node/_http_client.ts`:
- Around line 251-263: The httpValidation option is validated and stored in the
constructor but is never actually used when initializing the parser in the
tickOnSocket() method. The parser initialization code only references
insecureHTTPParser and the existing lenient logic, meaning the "relaxed" and
"insecure" modes for httpValidation have no effect on parsing behavior. Either
add logic in the parser initialization section (around lines 906-912 where
initialize() is called) to map the this.httpValidation modes to appropriate
parser flags alongside the existing lenient flag handling, or alternatively,
update the validation code to throw an error if httpValidation is set to
"relaxed" or "insecure" since those modes are not currently supported.
In `@src/js/node/_http_outgoing.ts`:
- Around line 106-133: The _isLenientHeaderValidation function is vulnerable to
prototype pollution because it uses regular property access that accepts
inherited values. To fix this, replace all property lookups for httpValidation
and insecureHTTPParser with own-property checks using
Object.prototype.hasOwnProperty.call() or Object.hasOwn(). This applies to
checks on this object, this.req?.socket?.server object, and any other objects
being accessed. Ensure that only values that are directly owned properties (not
inherited from the prototype chain) are used to determine the lenient validation
behavior.
In `@src/js/node/_http_server.ts`:
- Around line 2412-2416: The `writableCorked` property is throwing an error for
queued pipelined responses instead of returning a neutral cork count. Add a
guard check at the beginning of the `writableCorked` property implementation
that checks if `this[kPipelinedQueuedState] !== undefined` (matching the pattern
used for the socket check in the same section), and return 0 as the neutral cork
count for queued responses. Apply this same guard pattern to the related code at
lines 2450-2453 to ensure consistency across all cork-related operations.
- Around line 1172-1193: The kHttp2PrefaceStart array currently contains only
the first 16 bytes of the HTTP/2 connection preface, but the onServerClientError
function reports a 24-byte preface when returning HPE_PAUSED_H2_UPGRADE. This
causes the isHttp2Preface function to incorrectly match partial prefaces. Extend
the kHttp2PrefaceStart array to include the complete 24-byte HTTP/2 connection
preface by adding the remaining 8 bytes that follow "PRI * HTTP/2.0\r\n",
ensuring the matcher accurately identifies the full preface before the error is
reported.
- Around line 230-238: The releaseServerParserShim function calls parser.free()
directly, but since socket.parser is exposed to user code, users can replace or
tamper with the free method before teardown, causing the internal cleanup to
throw. Instead of calling the user-tamperable parser.free() method, use a
private symbol to store a reference to the actual internal parser release
function during parser creation, and call that private symbol reference directly
in releaseServerParserShim to ensure reliable teardown regardless of user code
modifications.
In `@src/jsc/bindings/node/JSNodeHTTPServerSocket.cpp`:
- Around line 408-416: The responses in the local pipelined vector become
unrooted when socket->m_pipelinedResponses is cleared via std::exchange before
Bun__NodeHTTPResponse_onClose callbacks execute, creating a GC safety issue if
GC occurs during callbacks. Either prevent clearing m_pipelinedResponses by
using a separate boolean flag (like m_notifyingResponses) to prevent reentrant
delivery while keeping the vector rooted and attached to the socket, or wrap the
pipelined responses in a GC-rooted Strong holder before invoking the callbacks
so they remain protected during callback execution.
- Around line 175-178: The trailer name and value strings on lines 175 and 177
are being constructed using fromUTF8ReplacingInvalidSequences(), which corrupts
raw HTTP trailer bytes (particularly obs-text bytes 0x80–0xFF) by replacing
invalid UTF-8 sequences with U+FFFD. Replace both
fromUTF8ReplacingInvalidSequences() calls with a Latin-1 string constructor that
creates a WTF::String directly from the raw bytes while preserving the exact
byte values as-is, mapping each byte 0–255 directly to Unicode code points
U+0000–U+00FF. Keep the same reinterpret_cast and data access patterns, but use
the appropriate Latin-1 constructor method instead of the UTF-8 replacement
function.
In `@src/jsc/bindings/node/JSNodeHTTPServerSocketPrototype.cpp`:
- Around line 108-111: Replace the `isTrue()` method call with
`toBoolean(globalObject)` in the argument being passed to the
`upgradeToTunnelMode` method. The current code uses
`callFrame->argument(0).isTrue()` which only accepts the literal boolean `true`,
but the documented behavior specifies "with a truthy argument," meaning any
JavaScript truthy value (numbers, strings, objects, etc.) should be accepted.
Change it to `callFrame->argument(0).toBoolean(globalObject)` to properly coerce
any truthy value to a boolean, matching the standard pattern used consistently
throughout the codebase.
- Around line 162-163: The conversion of headersTimeout and requestTimeout from
double to uint64_t lacks upper-bound checking, which can lead to undefined
behavior when a finite double value exceeds uint64_t::max and is cast. Add
defensive clamping to std::numeric_limits<uint64_t>::max() before casting both
headersTimeout and requestTimeout in the assignments to headersTimeoutMs and
requestTimeoutMs respectively, ensuring that any finite double value above the
maximum uint64_t is clamped to the maximum representable uint64_t value rather
than causing undefined behavior during the cast.
In `@src/jsc/bindings/NodeHTTP.cpp`:
- Around line 41-43: The JSFunction registration for jsHTTPSetCustomOptions at
line 1188 declares an incorrect arity of 2, while the actual C++ function
implementation asserts argumentCount() == 7 and the JavaScript call site passes
7 arguments. Locate the JSC::JSFunction::create call that registers
jsHTTPSetCustomOptions and change the arity parameter from 2 to 7 to match the
expected argument count of server, requireHostHeader, useStrictMethodValidation,
insecureHTTPParser, maxHeaderSize, onClientError, and onConnection.
In `@src/runtime/api/bun/h2_frame_parser.rs`:
- Around line 6286-6317: The SETTINGS state is being committed too early in the
update_settings method. The calls to self.local_settings.set(),
self.explicit_settings.set(), and self.custom_settings.with_mut() should be
moved to execute after the remoteCustomSettings parsing block completes, not
before. Currently, if the remoteCustomSettings array iteration or property
access fails (lines 6300-6317), the function returns an error but the staged
settings have already been installed, leaving the system in an inconsistent
state where the SETTINGS frame was never sent. Reorganize the code so that the
settings commit operations execute only after all fallible operations like the
remoteCustomSettings parsing are completed successfully.
In `@src/runtime/api/bun/h2/connection.rs`:
- Around line 618-631: The condition checking for unsolicited SETTINGS ACKs in
the block starting around line 620 has a logic error. Currently it only returns
false when both local_settings_acked is true AND pending_local_settings_acks is
empty, which allows the first unsolicited ACK to incorrectly pass through when
local_settings_acked is false. Fix this by simplifying the condition to return
false whenever pending_local_settings_acks is empty, regardless of the
local_settings_acked state. This ensures that ACKs are only processed when there
are actual pending settings to acknowledge.
In `@test/js/node/http2/h2-conformance.test.ts`:
- Around line 564-571: The write method in the Writable object uses
setTimeout(cb, 1) which introduces timing-sensitive behavior that can cause
flakiness under load. Replace this setTimeout call with setImmediate(cb) or
process.nextTick(cb) to provide a non-time-based async yield that maintains the
same backpressure simulation intent without relying on wall-clock timing.
---
Outside diff comments:
In `@src/js/node/http2.ts`:
- Around line 3020-3058: The issue is that when statOptions.length becomes zero
or negative (for empty files or when offset exceeds file size), the code still
creates a read stream with an invalid end value calculated on line 3057, causing
range errors. After computing statOptions.length on lines 3020-3023, add a
check: if statOptions.length is less than or equal to 0, call the
finishNativeStream() function to properly end the response without creating the
fileStream, then return early. This ensures zero-length responses are handled
correctly without attempting to create invalid read stream ranges.
- Around line 5469-5480: The client stream destruction path is not synchronously
destroying open streams before detaching the parser, unlike the
ServerHttp2Session.destroy() implementation which calls
destroyStreamForSessionDestroy() before emitErrorToAllStreams(). Add a
synchronous destruction of all open client streams using
destroyStreamForSessionDestroy() before the parser.emitErrorToAllStreams() call
to ensure streams transition to a cancelled/destroyed state immediately rather
than remaining writable during asynchronous native teardown.
- Around line 2947-3058: The `kOwnsFd` flag is being stored as persistent
mutable state on the stream, which causes problems when multiple response
operations occur on the same stream (e.g., if `respondWithFile()` fails and user
code falls back to `respondWithFD()`). Instead of checking `this[kOwnsFd]` in
the `doSendFileFD` function and relying on stream-level state, pass the fd
ownership information through the async callback chain as a parameter. This
ensures each response operation independently tracks whether it owns the fd,
rather than inheriting stale ownership state from previous failed operations.
Remove the stream-level `kOwnsFd` assignment and refactor the callback
signatures to carry the ownership flag through the operation sequence.
In `@src/runtime/api/bun/h2_frame_parser.rs`:
- Around line 6242-6274: The customSettings validation is incomplete and allows
standard HTTP/2 SETTINGS IDs (0-5) to be passed through, which can desynchronize
Bun's internal state. After parsing setting_id and validating it is within [0,
0xFFFF], add an additional check to reject any setting_id values that are in the
reserved standard range (0-5). Additionally, before casting setting_value to
u32, add validation to reject non-finite values (check for NaN and Infinity
using appropriate float methods) and reject fractional values (check if the
value differs from its truncated integer form) to prevent silent data loss
during the as u32 cast in the staged_custom.push call.
🪄 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: 10576267-97f5-4bcd-871c-84dafac1a4b2
📒 Files selected for processing (223)
packages/bun-usockets/src/context.cpackages/bun-usockets/src/crypto/openssl.cpackages/bun-usockets/src/internal/internal.hpackages/bun-usockets/src/libusockets.hpackages/bun-uws/src/App.hpackages/bun-uws/src/ChunkedEncoding.hpackages/bun-uws/src/HttpContext.hpackages/bun-uws/src/HttpContextData.hpackages/bun-uws/src/HttpErrors.hpackages/bun-uws/src/HttpParser.hpackages/bun-uws/src/HttpResponse.hpackages/bun-uws/src/HttpResponseData.hsrc/js/internal/http.tssrc/js/internal/timers.tssrc/js/internal/tls.tssrc/js/node/_http_client.tssrc/js/node/_http_common.tssrc/js/node/_http_incoming.tssrc/js/node/_http_outgoing.tssrc/js/node/_http_server.tssrc/js/node/domain.tssrc/js/node/http2.tssrc/js/node/https.tssrc/js/node/net.tssrc/js/node/tls.tssrc/jsc/ErrorCode.rssrc/jsc/bindings/ErrorCode.tssrc/jsc/bindings/NodeHTTP.cppsrc/jsc/bindings/node/JSNodeHTTPServerSocket.cppsrc/jsc/bindings/node/JSNodeHTTPServerSocket.hsrc/jsc/bindings/node/JSNodeHTTPServerSocketPrototype.cppsrc/runtime/api/bun/h2/connection.rssrc/runtime/api/bun/h2/wire.rssrc/runtime/api/bun/h2_frame_parser.rssrc/runtime/api/h2.classes.tssrc/runtime/server/mod.rssrc/runtime/server/server_body.rssrc/uws_sys/App.rssrc/uws_sys/libuwsockets.cpptest/expectations.txttest/js/bun/test/parallel/test-http-host-array-should-throw-in-request.tstest/js/bun/test/parallel/test-http-should-emit-timeout-event-when-using-server-setTimeout.tstest/js/bun/test/parallel/test-http-should-emit-timeout-event.tstest/js/node/http/node-http.test.tstest/js/node/http2/h2-conformance.test.tstest/js/node/http2/node-http2.test.jstest/js/node/net/node-net.test.tstest/js/node/test/common/index.jstest/js/node/test/parallel/test-http-abort-stream-end.jstest/js/node/test/parallel/test-http-agent-domain-reused-gc.jstest/js/node/test/parallel/test-http-agent-keepalive-delay.jstest/js/node/test/parallel/test-http-agent-maxtotalsockets.jstest/js/node/test/parallel/test-http-agent-remove.jstest/js/node/test/parallel/test-http-allow-content-length-304.jstest/js/node/test/parallel/test-http-autoselectfamily.jstest/js/node/test/parallel/test-http-buffer-sanity.jstest/js/node/test/parallel/test-http-chunk-extensions-limit.jstest/js/node/test/parallel/test-http-chunk-problem.jstest/js/node/test/parallel/test-http-client-abort-keep-alive-queued-unix-socket.jstest/js/node/test/parallel/test-http-client-abort-unix-socket.jstest/js/node/test/parallel/test-http-client-close-with-default-agent.jstest/js/node/test/parallel/test-http-client-finished.jstest/js/node/test/parallel/test-http-client-immediate-error.jstest/js/node/test/parallel/test-http-client-keep-alive-hint.jstest/js/node/test/parallel/test-http-client-pipe-end.jstest/js/node/test/parallel/test-http-client-reject-unexpected-agent.jstest/js/node/test/parallel/test-http-client-request-options.jstest/js/node/test/parallel/test-http-client-response-domain.jstest/js/node/test/parallel/test-http-client-response-timeout.jstest/js/node/test/parallel/test-http-client-spurious-aborted.jstest/js/node/test/parallel/test-http-client-timeout-event.jstest/js/node/test/parallel/test-http-client-timeout-on-connect.jstest/js/node/test/parallel/test-http-client-timeout-option.jstest/js/node/test/parallel/test-http-client-timeout.jstest/js/node/test/parallel/test-http-client-with-create-connection.jstest/js/node/test/parallel/test-http-connect-req-res.jstest/js/node/test/parallel/test-http-connect.jstest/js/node/test/parallel/test-http-content-length-mismatch.jstest/js/node/test/parallel/test-http-correct-hostname.jstest/js/node/test/parallel/test-http-date-header.jstest/js/node/test/parallel/test-http-decoded-auth.jstest/js/node/test/parallel/test-http-double-content-length.jstest/js/node/test/parallel/test-http-dump-req-when-res-ends.jstest/js/node/test/parallel/test-http-early-hints-invalid-argument.jstest/js/node/test/parallel/test-http-end-throw-socket-handling.jstest/js/node/test/parallel/test-http-expect-handling.jstest/js/node/test/parallel/test-http-extra-response.jstest/js/node/test/parallel/test-http-flush-headers.jstest/js/node/test/parallel/test-http-flush-response-headers.jstest/js/node/test/parallel/test-http-generic-streams.jstest/js/node/test/parallel/test-http-head-throw-on-response-body-write.jstest/js/node/test/parallel/test-http-header-badrequest.jstest/js/node/test/parallel/test-http-header-obstext.jstest/js/node/test/parallel/test-http-header-read.jstest/js/node/test/parallel/test-http-header-value-relaxed.jstest/js/node/test/parallel/test-http-highwatermark.jstest/js/node/test/parallel/test-http-host-headers.jstest/js/node/test/parallel/test-http-hostname-typechecking.jstest/js/node/test/parallel/test-http-incoming-pipelined-socket-destroy.jstest/js/node/test/parallel/test-http-insecure-parser-per-stream.jstest/js/node/test/parallel/test-http-insecure-parser.jstest/js/node/test/parallel/test-http-invalidheaderfield.jstest/js/node/test/parallel/test-http-invalidheaderfield2.jstest/js/node/test/parallel/test-http-keep-alive-drop-requests.jstest/js/node/test/parallel/test-http-keep-alive-empty-line.mjstest/js/node/test/parallel/test-http-keep-alive-max-requests.jstest/js/node/test/parallel/test-http-localaddress.jstest/js/node/test/parallel/test-http-many-ended-pipelines.jstest/js/node/test/parallel/test-http-max-header-size-per-stream.jstest/js/node/test/parallel/test-http-max-http-headers.jstest/js/node/test/parallel/test-http-multiple-headers.jstest/js/node/test/parallel/test-http-no-read-no-dump.jstest/js/node/test/parallel/test-http-outgoing-drain-writable-length.jstest/js/node/test/parallel/test-http-outgoing-finished.jstest/js/node/test/parallel/test-http-outgoing-proto.jstest/js/node/test/parallel/test-http-outgoing-renderHeaders.jstest/js/node/test/parallel/test-http-parser-finish-error.jstest/js/node/test/parallel/test-http-parser-free.jstest/js/node/test/parallel/test-http-parser-freed-before-upgrade.jstest/js/node/test/parallel/test-http-parser-freed-during-execute.jstest/js/node/test/parallel/test-http-parser-memory-retention.jstest/js/node/test/parallel/test-http-parser-multiple-execute.jstest/js/node/test/parallel/test-http-parser-timeout-reset.jstest/js/node/test/parallel/test-http-parser.jstest/js/node/test/parallel/test-http-pause.jstest/js/node/test/parallel/test-http-pipeline-assertionerror-finish.jstest/js/node/test/parallel/test-http-pipeline-flood.jstest/js/node/test/parallel/test-http-pipeline-outgoing-destroy.jstest/js/node/test/parallel/test-http-proxy.jstest/js/node/test/parallel/test-http-raw-headers.jstest/js/node/test/parallel/test-http-readable-data-event.jstest/js/node/test/parallel/test-http-req-close-robust-from-tampering.jstest/js/node/test/parallel/test-http-req-res-close.jstest/js/node/test/parallel/test-http-request-end-twice.jstest/js/node/test/parallel/test-http-request-end.jstest/js/node/test/parallel/test-http-request-method-delete-payload.jstest/js/node/test/parallel/test-http-response-add-header-after-sent.jstest/js/node/test/parallel/test-http-response-readable.jstest/js/node/test/parallel/test-http-response-remove-header-after-sent.jstest/js/node/test/parallel/test-http-response-setheaders.jstest/js/node/test/parallel/test-http-response-status-message.jstest/js/node/test/parallel/test-http-response-statuscode.jstest/js/node/test/parallel/test-http-response-writehead-returns-this.jstest/js/node/test/parallel/test-http-same-map.jstest/js/node/test/parallel/test-http-server-client-error.jstest/js/node/test/parallel/test-http-server-close-all.jstest/js/node/test/parallel/test-http-server-close-idle-wait-response.jstest/js/node/test/parallel/test-http-server-close-idle.jstest/js/node/test/parallel/test-http-server-connection-list-when-close.jstest/js/node/test/parallel/test-http-server-destroy-socket-on-client-error.jstest/js/node/test/parallel/test-http-server-drop-connections-in-cluster.jstest/js/node/test/parallel/test-http-server-headers-timeout-delayed-headers.jstest/js/node/test/parallel/test-http-server-headers-timeout-interrupted-headers.jstest/js/node/test/parallel/test-http-server-headers-timeout-keepalive.jstest/js/node/test/parallel/test-http-server-headers-timeout-pipelining.jstest/js/node/test/parallel/test-http-server-keep-alive-timeout.jstest/js/node/test/parallel/test-http-server-keepalive-end.jstest/js/node/test/parallel/test-http-server-method.query.jstest/js/node/test/parallel/test-http-server-multiheaders.jstest/js/node/test/parallel/test-http-server-multiple-client-error.jstest/js/node/test/parallel/test-http-server-non-utf8-header.jstest/js/node/test/parallel/test-http-server-options-highwatermark.jstest/js/node/test/parallel/test-http-server-options-incoming-message.jstest/js/node/test/parallel/test-http-server-options-server-response.jstest/js/node/test/parallel/test-http-server-reject-chunked-with-content-length.jstest/js/node/test/parallel/test-http-server-reject-cr-no-lf.jstest/js/node/test/parallel/test-http-server-request-timeout-delayed-body.jstest/js/node/test/parallel/test-http-server-request-timeout-delayed-headers.jstest/js/node/test/parallel/test-http-server-request-timeout-interrupted-body.jstest/js/node/test/parallel/test-http-server-request-timeout-interrupted-headers.jstest/js/node/test/parallel/test-http-server-request-timeout-keepalive.jstest/js/node/test/parallel/test-http-server-request-timeout-pipelining.jstest/js/node/test/parallel/test-http-server-request-timeout-upgrade.jstest/js/node/test/parallel/test-http-server-stale-close.jstest/js/node/test/parallel/test-http-server-unconsume.jstest/js/node/test/parallel/test-http-server.jstest/js/node/test/parallel/test-http-set-cookies.jstest/js/node/test/parallel/test-http-set-header-chain.jstest/js/node/test/parallel/test-http-set-timeout-server.jstest/js/node/test/parallel/test-http-set-timeout.jstest/js/node/test/parallel/test-http-set-trailers.jstest/js/node/test/parallel/test-http-socket-encoding-error.jstest/js/node/test/parallel/test-http-status-code.jstest/js/node/test/parallel/test-http-status-message.jstest/js/node/test/parallel/test-http-timeout-overflow.jstest/js/node/test/parallel/test-http-transfer-encoding-repeated-chunked.jstest/js/node/test/parallel/test-http-unix-socket.jstest/js/node/test/parallel/test-http-upgrade-server-callback.jstest/js/node/test/parallel/test-http-upgrade-server-with-body-and-extras.mjstest/js/node/test/parallel/test-http-upgrade-server-with-body-error.mjstest/js/node/test/parallel/test-http-upgrade-server-with-body.mjstest/js/node/test/parallel/test-http-upgrade-server-with-large-body-unread.mjstest/js/node/test/parallel/test-http-upgrade-server-with-large-body.mjstest/js/node/test/parallel/test-http-url.parse-basic.jstest/js/node/test/parallel/test-http-url.parse-https.request.jstest/js/node/test/parallel/test-http-write-callbacks.jstest/js/node/test/parallel/test-http-zero-length-write.jstest/js/node/test/parallel/test-https-agent-additional-options.jstest/js/node/test/parallel/test-https-agent-keylog.jstest/js/node/test/parallel/test-https-agent-session-eviction.jstest/js/node/test/parallel/test-https-agent-sni.jstest/js/node/test/parallel/test-https-agent.jstest/js/node/test/parallel/test-https-argument-of-creating.jstest/js/node/test/parallel/test-https-autoselectfamily.jstest/js/node/test/parallel/test-https-byteswritten.jstest/js/node/test/parallel/test-https-client-renegotiation-limit.jstest/js/node/test/parallel/test-https-insecure-parse-per-stream.jstest/js/node/test/parallel/test-https-keep-alive-drop-requests.jstest/js/node/test/parallel/test-https-localaddress.jstest/js/node/test/parallel/test-https-max-header-size-per-stream.jstest/js/node/test/parallel/test-https-max-headers-count.jstest/js/node/test/parallel/test-https-options-boolean-check.jstest/js/node/test/parallel/test-https-pfx.jstest/js/node/test/parallel/test-https-resume-after-renew.jstest/js/node/test/parallel/test-https-selfsigned-no-keycertsign-no-crash.jstest/js/node/test/parallel/test-https-server-close-all.jstest/js/node/test/parallel/test-https-server-close-idle.jstest/js/node/test/parallel/test-https-set-timeout-server.jstest/js/node/test/parallel/test-https-strict.jstest/js/node/test/parallel/test-https-timeout-server-2.jstest/js/node/test/parallel/test-https-timeout-server.jstest/js/node/test/parallel/test-https-unix-socket-self-signed.jstest/js/node/test/parallel/test-tls-options-boolean-check.js
💤 Files with no reviewable changes (62)
- test/js/node/test/parallel/test-http-socket-encoding-error.js
- test/js/node/test/parallel/test-https-agent.js
- test/js/node/test/parallel/test-http-server-request-timeout-pipelining.js
- test/js/node/test/parallel/test-http-status-message.js
- test/js/node/test/parallel/test-http-timeout-overflow.js
- test/js/node/test/parallel/test-https-timeout-server.js
- test/js/node/test/parallel/test-http-client-with-create-connection.js
- test/js/node/test/parallel/test-http-server-reject-chunked-with-content-length.js
- test/js/node/test/parallel/test-http-unix-socket.js
- test/js/node/test/parallel/test-http-server-stale-close.js
- test/js/node/test/parallel/test-https-byteswritten.js
- test/js/node/test/parallel/test-https-agent-session-eviction.js
- test/js/node/test/parallel/test-http-client-response-timeout.js
- test/js/node/test/parallel/test-http-server-unconsume.js
- test/js/node/test/parallel/test-http-set-header-chain.js
- test/js/node/test/parallel/test-http-set-cookies.js
- test/js/node/test/parallel/test-http-upgrade-server-with-body-error.mjs
- test/js/node/test/parallel/test-https-client-renegotiation-limit.js
- test/js/node/test/parallel/test-http-url.parse-https.request.js
- test/js/node/test/parallel/test-https-selfsigned-no-keycertsign-no-crash.js
- test/js/node/test/parallel/test-http-server-reject-cr-no-lf.js
- test/js/node/test/parallel/test-https-unix-socket-self-signed.js
- test/js/node/test/parallel/test-https-pfx.js
- test/js/node/test/parallel/test-http-write-callbacks.js
- test/js/node/test/parallel/test-https-agent-keylog.js
- test/js/node/test/parallel/test-tls-options-boolean-check.js
- test/js/node/test/parallel/test-https-resume-after-renew.js
- test/js/node/test/parallel/test-https-strict.js
- test/js/node/test/parallel/test-https-keep-alive-drop-requests.js
- test/js/node/test/parallel/test-http-url.parse-basic.js
- test/js/node/test/parallel/test-http-client-pipe-end.js
- test/js/node/test/parallel/test-https-localaddress.js
- test/js/node/test/parallel/test-http-server-request-timeout-upgrade.js
- test/js/node/test/parallel/test-https-max-header-size-per-stream.js
- test/js/node/test/parallel/test-http-upgrade-server-with-large-body-unread.mjs
- test/js/node/test/parallel/test-http-zero-length-write.js
- test/js/node/test/parallel/test-http-transfer-encoding-repeated-chunked.js
- test/js/node/test/parallel/test-http-status-code.js
- test/js/node/test/parallel/test-https-argument-of-creating.js
- test/js/node/test/parallel/test-http-upgrade-server-with-body-and-extras.mjs
- test/js/node/test/parallel/test-https-agent-additional-options.js
- test/js/node/test/parallel/test-https-max-headers-count.js
- test/js/node/test/parallel/test-http-server-request-timeout-delayed-body.js
- test/js/node/test/parallel/test-http-server.js
- test/js/node/test/parallel/test-http-server-request-timeout-interrupted-headers.js
- test/js/node/test/parallel/test-http-set-timeout-server.js
- test/js/node/test/parallel/test-http-set-timeout.js
- test/js/node/test/parallel/test-https-autoselectfamily.js
- test/js/node/test/parallel/test-http-upgrade-server-with-body.mjs
- test/js/node/test/parallel/test-http-server-request-timeout-delayed-headers.js
- test/js/node/test/parallel/test-https-options-boolean-check.js
- test/js/node/test/parallel/test-http-server-request-timeout-interrupted-body.js
- test/js/node/test/parallel/test-https-timeout-server-2.js
- test/js/node/test/parallel/test-https-server-close-all.js
- test/js/node/test/parallel/test-http-upgrade-server-with-large-body.mjs
- test/js/node/test/parallel/test-https-set-timeout-server.js
- test/js/node/test/parallel/test-http-upgrade-server-callback.js
- test/js/node/test/parallel/test-http-set-trailers.js
- test/js/node/test/parallel/test-https-agent-sni.js
- test/js/node/test/parallel/test-http-server-request-timeout-keepalive.js
- test/js/node/test/parallel/test-https-server-close-idle.js
- test/js/node/test/parallel/test-https-insecure-parse-per-stream.js
0713d35 to
3c41a0c
Compare
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/runtime/api/bun/h2_frame_parser.rs (1)
6101-6109: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject non-finite SETTINGS numbers before casting.
NaNpasses these</>range checks and then casts to0, sosession.settings({ maxFrameSize: NaN })orcustomSettings: { 10: NaN }can silently serialize the wrong value instead of throwing.Suggested guard
- if value < ($min as f64) || value > $max { + if !value.is_finite() || value < ($min as f64) || value > $max { return global_object .err_http2_invalid_setting_value_range_error($err) .throw(); }- if value < 0.0 || value > MAX_WINDOW_SIZE_F64 { + if !value.is_finite() || value < 0.0 || value > MAX_WINDOW_SIZE_F64 { return global_object .err_http2_invalid_setting_value_range_error( "Expected initialWindowSize to be a number between 0 and 2^32-1",- if value < 0.0 || value > MAX_HEADER_TABLE_SIZE_F64 { + if !value.is_finite() || value < 0.0 || value > MAX_HEADER_TABLE_SIZE_F64 { return global_object .err_http2_invalid_setting_value_range_error( "Invalid custom setting value",As per coding guidelines, “Numbers from JS or the wire: handle NaN, ±Infinity, negatives, out-of-range before casting.”
Also applies to: 6143-6155, 6267-6276
🤖 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/runtime/api/bun/h2_frame_parser.rs` around lines 6101 - 6109, In the h2_frame_parser.rs file where SETTINGS values are validated, add a check to ensure the number value is finite before performing the range validation. After getting the value from v.as_number() but before the existing range check using `<` and `>` operators, insert a guard condition to reject NaN and ±Infinity values (e.g., check that value.is_finite() returns true). If the value is not finite, throw the invalid setting value range error. Apply this same fix pattern to all three locations mentioned: the initial range check block with the `value < ($min as f64) || value > $max` condition, and the two other similar validation blocks at the additional line ranges referenced.Source: Coding guidelines
src/js/node/http2.ts (1)
2947-2955: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winCapture FD ownership per file-send operation.
kOwnsFdis stream-global butrespondWithFile()andrespondWithFD()both complete asynchronously. If a failed/cancelledrespondWithFile()is followed byrespondWithFD(), or a second response attempt races the firstfs.open/fstat, later callbacks can observe the wrong ownership and either close a caller-owned fd or leak an fd opened byrespondWithFile(). PassownsFdintodoSendFileFD()/afterOpen()and use that local everywhere instead of reading mutable stream state.Suggested direction
-function doSendFileFD(options, fd, headers, err, stat) { +function doSendFileFD(options, fd, headers, ownsFd, err, stat) { const onError = options.onError; - const ownsFd = this[kOwnsFd] === true; if (err) { if (ownsFd && err.code !== "EBADF") { tryClose(fd); @@ - if (this[kOwnsFd] === true) tryClose(fd); + if (ownsFd) tryClose(fd); return; } @@ - if (this[kOwnsFd] === true) tryClose(fd); + if (ownsFd) tryClose(fd); @@ - autoClose: this[kOwnsFd] === true, + autoClose: ownsFd,function afterOpen(options, headers, err, fd) { @@ - fs.fstat(fd, doSendFileFD.bind(this, options, fd, headers)); + fs.fstat(fd, doSendFileFD.bind(this, options, fd, headers, true)); } @@ - this[kOwnsFd] = true; fs.open(path, "r", afterOpen.bind(this, options || {}, headers)); @@ - fs.fstat(fd.fd, doSendFileFD.bind(this, options, fd, headers)); + fs.fstat(fd.fd, doSendFileFD.bind(this, options, fd.fd, headers, false)); } else { - fs.fstat(fd, doSendFileFD.bind(this, options, fd, headers)); + fs.fstat(fd, doSendFileFD.bind(this, options, fd, headers, false)); }Also applies to: 3011-3016, 3050-3058, 3285-3286, 3344-3354
🤖 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/js/node/http2.ts` around lines 2947 - 2955, The doSendFileFD function and afterOpen function are reading kOwnsFd from mutable stream state, which causes race conditions when respondWithFile and respondWithFD execute concurrently or when operations fail and retry. Refactor these functions to accept ownsFd as a parameter instead of reading this[kOwnsFd] directly. Update all call sites to doSendFileFD and afterOpen (including the locations around lines 3011-3016, 3050-3058, 3285-3286, and 3344-3354) to pass the ownsFd value as an argument at the time of the call, ensuring each async operation captures its own ownership state rather than referencing shared mutable state.
🤖 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 `@packages/bun-uws/src/ChunkedEncoding.h`:
- Around line 203-215: The maxTrailerSectionSize bounds check is occurring after
the completion check for isCompleteTrailerSection, allowing an oversized trailer
section to be accepted as complete if it terminates with a newline character.
Reorder the validation logic so that the maxTrailerSectionSize check happens
before the completion check that returns the fin chunk. This ensures that any
trailer section exceeding the size limit is rejected before being treated as a
complete message, preventing the acceptance of malformed or adversarial data.
In `@packages/bun-uws/src/HttpContext.h`:
- Around line 727-736: The condition on the if statement checking
nodeHttpQueuedPipelinedCount only preserves half-open sockets when there are
queued pipelined responses, but it fails to account for a current in-flight
response that is still pending or being written. Modify the if condition to also
check whether there is an active in-flight response in addition to checking for
queued pipelined responses. This ensures the connection remains open long enough
to write the current response even when no subsequent responses are queued. The
fix should ensure that nodeHttpReceivedFIN is set to true whenever either a
current response is in-flight OR pipelined responses are queued.
In `@src/js/node/_http_server.ts`:
- Around line 340-342: The code uses `Array.isArray(ca)` which can be tampered
with by overriding user globals and prototypes, violating tamper-resistance
requirements. Replace `Array.isArray(ca)` with the tamper-safe intrinsic
`$isArray(ca)` in the conditional expression. Additionally, locate the
`uniqueHeaders.has(key)` call referenced in lines 2012-2016 and replace it with
the corresponding Set intrinsic to maintain consistency with the
tamper-resistance coding guidelines for built-in JS modules.
- Around line 2290-2305: The bufferPipelinedWrite function accepts and queues
invalid chunk values before validating them, which corrupts the queued.bytes
counter when chunk.length is read later. Apply the same chunk validation logic
used in the non-queued write/end path to validate chunk types and values (such
as null, 0, or plain objects) before pushing to queued.ops.push. This ensures
invalid chunks are rejected synchronously with proper errors rather than
deferring failures until replay, preventing queued.bytes corruption.
- Around line 711-728: The HTTPS state flag is restored at line 817, but early
returns in the CONNECT handler path (around lines 773/778) cause the flag to not
be restored, leaving stale state for subsequent requests. Additionally, since
the flag is used across user-configurable constructors like RequestClass,
bracket its restoration more tightly. Move the restoration of the previous HTTPS
state (using prevIsNextIncomingMessageHTTPS) from line 817 to immediately after
the RequestClass construction at line 727, ensuring the flag is restored right
after its intended use rather than much later in the control flow.
In `@src/js/node/https.ts`:
- Around line 514-517: The ALPN defaulting condition in the https.ts file uses
strict equality checks for undefined, which causes misalignment with Node's
behavior when ALPNProtocols is explicitly set to null. Change the condition from
checking `options.ALPNProtocols === undefined && options.ALPNCallback ===
undefined` to use falsy checks instead (such as `!options.ALPNProtocols &&
!options.ALPNCallback`) so that any falsy value (null, undefined, etc.) is
treated as "not set" and triggers the default protocol assignment of http/1.1,
matching Node v26.3.0's behavior.
In `@src/runtime/api/bun/h2_frame_parser.rs`:
- Around line 6288-6290: The settings state is being committed to the parser too
early in `load_settings_from_js_value()`. The three set calls for
local_settings, explicit_settings, and custom_settings at lines 6288-6290 should
not execute immediately because if `set_settings()` later rejects the frame due
to maxOutstandingSettings being exceeded, the parser state has already been
modified with the rejected settings. Instead, keep these settings staged and
return them from `load_settings_from_js_value()` without committing them. Then
modify the code path in `set_settings()` that handles the outstanding-settings
gate to only call these three set methods after confirming the frame is
accepted. Apply the same fix pattern to the other occurrence mentioned at lines
6330-6332.
- Around line 2474-2480: The pending_settings_window_submissions structure only
captures standard settings via settings.to_engine_settings() but does not
include custom_settings, which gets attached later in on_local_settings(). This
causes ACKs to potentially report custom settings from a later submission or
miss them entirely. Modify the PendingLocalSettings structure to include a
custom_settings field alongside the standard settings field, then capture
self.custom_settings when pushing to pending_settings_window_submissions (both
at the current location and at the other referenced location around lines
5631-5645). Finally, update on_local_settings() to use the custom_settings from
the ACK-specific pending submission snapshot instead of self.custom_settings
when building localSettings.customSettings.
---
Outside diff comments:
In `@src/js/node/http2.ts`:
- Around line 2947-2955: The doSendFileFD function and afterOpen function are
reading kOwnsFd from mutable stream state, which causes race conditions when
respondWithFile and respondWithFD execute concurrently or when operations fail
and retry. Refactor these functions to accept ownsFd as a parameter instead of
reading this[kOwnsFd] directly. Update all call sites to doSendFileFD and
afterOpen (including the locations around lines 3011-3016, 3050-3058, 3285-3286,
and 3344-3354) to pass the ownsFd value as an argument at the time of the call,
ensuring each async operation captures its own ownership state rather than
referencing shared mutable state.
In `@src/runtime/api/bun/h2_frame_parser.rs`:
- Around line 6101-6109: In the h2_frame_parser.rs file where SETTINGS values
are validated, add a check to ensure the number value is finite before
performing the range validation. After getting the value from v.as_number() but
before the existing range check using `<` and `>` operators, insert a guard
condition to reject NaN and ±Infinity values (e.g., check that value.is_finite()
returns true). If the value is not finite, throw the invalid setting value range
error. Apply this same fix pattern to all three locations mentioned: the initial
range check block with the `value < ($min as f64) || value > $max` condition,
and the two other similar validation blocks at the additional line ranges
referenced.
🪄 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: fa9d9a7c-7256-474a-9cbf-44eeec920a42
📒 Files selected for processing (237)
packages/bun-usockets/src/context.cpackages/bun-usockets/src/crypto/openssl.cpackages/bun-usockets/src/internal/internal.hpackages/bun-usockets/src/libusockets.hpackages/bun-uws/src/App.hpackages/bun-uws/src/ChunkedEncoding.hpackages/bun-uws/src/HttpContext.hpackages/bun-uws/src/HttpContextData.hpackages/bun-uws/src/HttpErrors.hpackages/bun-uws/src/HttpParser.hpackages/bun-uws/src/HttpResponse.hpackages/bun-uws/src/HttpResponseData.hsrc/js/internal/http.tssrc/js/internal/timers.tssrc/js/internal/tls.tssrc/js/node/_http_client.tssrc/js/node/_http_common.tssrc/js/node/_http_incoming.tssrc/js/node/_http_outgoing.tssrc/js/node/_http_server.tssrc/js/node/domain.tssrc/js/node/http2.tssrc/js/node/https.tssrc/js/node/net.tssrc/js/node/tls.tssrc/jsc/ErrorCode.rssrc/jsc/bindings/ErrorCode.tssrc/jsc/bindings/NodeHTTP.cppsrc/jsc/bindings/node/JSNodeHTTPServerSocket.cppsrc/jsc/bindings/node/JSNodeHTTPServerSocket.hsrc/jsc/bindings/node/JSNodeHTTPServerSocketPrototype.cppsrc/runtime/api/bun/h2/connection.rssrc/runtime/api/bun/h2/wire.rssrc/runtime/api/bun/h2_frame_parser.rssrc/runtime/api/h2.classes.tssrc/runtime/server/mod.rssrc/runtime/server/server_body.rssrc/uws_sys/App.rssrc/uws_sys/libuwsockets.cpptest/expectations.txttest/js/bun/test/parallel/test-http-host-array-should-throw-in-request.tstest/js/bun/test/parallel/test-http-should-emit-timeout-event-when-using-server-setTimeout.tstest/js/bun/test/parallel/test-http-should-emit-timeout-event.tstest/js/node/http/node-http.test.tstest/js/node/http2/h2-conformance.test.tstest/js/node/http2/node-http2.test.jstest/js/node/net/node-net.test.tstest/js/node/test/common/index.jstest/js/node/test/parallel/test-http-abort-stream-end.jstest/js/node/test/parallel/test-http-agent-domain-reused-gc.jstest/js/node/test/parallel/test-http-agent-keepalive-delay.jstest/js/node/test/parallel/test-http-agent-maxtotalsockets.jstest/js/node/test/parallel/test-http-agent-remove.jstest/js/node/test/parallel/test-http-allow-content-length-304.jstest/js/node/test/parallel/test-http-autoselectfamily.jstest/js/node/test/parallel/test-http-buffer-sanity.jstest/js/node/test/parallel/test-http-chunk-extensions-limit.jstest/js/node/test/parallel/test-http-chunk-problem.jstest/js/node/test/parallel/test-http-client-abort-keep-alive-queued-unix-socket.jstest/js/node/test/parallel/test-http-client-abort-unix-socket.jstest/js/node/test/parallel/test-http-client-close-with-default-agent.jstest/js/node/test/parallel/test-http-client-finished.jstest/js/node/test/parallel/test-http-client-immediate-error.jstest/js/node/test/parallel/test-http-client-keep-alive-hint.jstest/js/node/test/parallel/test-http-client-pipe-end.jstest/js/node/test/parallel/test-http-client-reject-unexpected-agent.jstest/js/node/test/parallel/test-http-client-request-options.jstest/js/node/test/parallel/test-http-client-response-domain.jstest/js/node/test/parallel/test-http-client-response-timeout.jstest/js/node/test/parallel/test-http-client-spurious-aborted.jstest/js/node/test/parallel/test-http-client-timeout-event.jstest/js/node/test/parallel/test-http-client-timeout-on-connect.jstest/js/node/test/parallel/test-http-client-timeout-option.jstest/js/node/test/parallel/test-http-client-timeout.jstest/js/node/test/parallel/test-http-client-with-create-connection.jstest/js/node/test/parallel/test-http-connect-req-res.jstest/js/node/test/parallel/test-http-connect.jstest/js/node/test/parallel/test-http-content-length-mismatch.jstest/js/node/test/parallel/test-http-correct-hostname.jstest/js/node/test/parallel/test-http-date-header.jstest/js/node/test/parallel/test-http-decoded-auth.jstest/js/node/test/parallel/test-http-double-content-length.jstest/js/node/test/parallel/test-http-dump-req-when-res-ends.jstest/js/node/test/parallel/test-http-early-hints-invalid-argument.jstest/js/node/test/parallel/test-http-end-throw-socket-handling.jstest/js/node/test/parallel/test-http-expect-handling.jstest/js/node/test/parallel/test-http-extra-response.jstest/js/node/test/parallel/test-http-flush-headers.jstest/js/node/test/parallel/test-http-flush-response-headers.jstest/js/node/test/parallel/test-http-generic-streams.jstest/js/node/test/parallel/test-http-head-throw-on-response-body-write.jstest/js/node/test/parallel/test-http-header-badrequest.jstest/js/node/test/parallel/test-http-header-obstext.jstest/js/node/test/parallel/test-http-header-read.jstest/js/node/test/parallel/test-http-header-value-relaxed.jstest/js/node/test/parallel/test-http-highwatermark.jstest/js/node/test/parallel/test-http-host-headers.jstest/js/node/test/parallel/test-http-hostname-typechecking.jstest/js/node/test/parallel/test-http-incoming-pipelined-socket-destroy.jstest/js/node/test/parallel/test-http-insecure-parser-per-stream.jstest/js/node/test/parallel/test-http-insecure-parser.jstest/js/node/test/parallel/test-http-invalidheaderfield.jstest/js/node/test/parallel/test-http-invalidheaderfield2.jstest/js/node/test/parallel/test-http-keep-alive-drop-requests.jstest/js/node/test/parallel/test-http-keep-alive-empty-line.mjstest/js/node/test/parallel/test-http-keep-alive-max-requests.jstest/js/node/test/parallel/test-http-localaddress.jstest/js/node/test/parallel/test-http-many-ended-pipelines.jstest/js/node/test/parallel/test-http-max-header-size-per-stream.jstest/js/node/test/parallel/test-http-max-http-headers.jstest/js/node/test/parallel/test-http-multiple-headers.jstest/js/node/test/parallel/test-http-no-read-no-dump.jstest/js/node/test/parallel/test-http-outgoing-drain-writable-length.jstest/js/node/test/parallel/test-http-outgoing-finished.jstest/js/node/test/parallel/test-http-outgoing-proto.jstest/js/node/test/parallel/test-http-outgoing-renderHeaders.jstest/js/node/test/parallel/test-http-parser-finish-error.jstest/js/node/test/parallel/test-http-parser-free.jstest/js/node/test/parallel/test-http-parser-freed-before-upgrade.jstest/js/node/test/parallel/test-http-parser-freed-during-execute.jstest/js/node/test/parallel/test-http-parser-memory-retention.jstest/js/node/test/parallel/test-http-parser-multiple-execute.jstest/js/node/test/parallel/test-http-parser-timeout-reset.jstest/js/node/test/parallel/test-http-parser.jstest/js/node/test/parallel/test-http-pause.jstest/js/node/test/parallel/test-http-pipeline-assertionerror-finish.jstest/js/node/test/parallel/test-http-pipeline-flood.jstest/js/node/test/parallel/test-http-pipeline-outgoing-destroy.jstest/js/node/test/parallel/test-http-proxy.jstest/js/node/test/parallel/test-http-raw-headers.jstest/js/node/test/parallel/test-http-readable-data-event.jstest/js/node/test/parallel/test-http-req-close-robust-from-tampering.jstest/js/node/test/parallel/test-http-req-res-close.jstest/js/node/test/parallel/test-http-request-end-twice.jstest/js/node/test/parallel/test-http-request-end.jstest/js/node/test/parallel/test-http-request-method-delete-payload.jstest/js/node/test/parallel/test-http-response-add-header-after-sent.jstest/js/node/test/parallel/test-http-response-readable.jstest/js/node/test/parallel/test-http-response-remove-header-after-sent.jstest/js/node/test/parallel/test-http-response-setheaders.jstest/js/node/test/parallel/test-http-response-status-message.jstest/js/node/test/parallel/test-http-response-statuscode.jstest/js/node/test/parallel/test-http-response-writehead-returns-this.jstest/js/node/test/parallel/test-http-same-map.jstest/js/node/test/parallel/test-http-server-client-error.jstest/js/node/test/parallel/test-http-server-close-all.jstest/js/node/test/parallel/test-http-server-close-idle-wait-response.jstest/js/node/test/parallel/test-http-server-close-idle.jstest/js/node/test/parallel/test-http-server-connection-list-when-close.jstest/js/node/test/parallel/test-http-server-destroy-socket-on-client-error.jstest/js/node/test/parallel/test-http-server-drop-connections-in-cluster.jstest/js/node/test/parallel/test-http-server-headers-timeout-delayed-headers.jstest/js/node/test/parallel/test-http-server-headers-timeout-interrupted-headers.jstest/js/node/test/parallel/test-http-server-headers-timeout-keepalive.jstest/js/node/test/parallel/test-http-server-headers-timeout-pipelining.jstest/js/node/test/parallel/test-http-server-keep-alive-timeout.jstest/js/node/test/parallel/test-http-server-keepalive-end.jstest/js/node/test/parallel/test-http-server-method.query.jstest/js/node/test/parallel/test-http-server-multiheaders.jstest/js/node/test/parallel/test-http-server-multiple-client-error.jstest/js/node/test/parallel/test-http-server-non-utf8-header.jstest/js/node/test/parallel/test-http-server-options-highwatermark.jstest/js/node/test/parallel/test-http-server-options-incoming-message.jstest/js/node/test/parallel/test-http-server-options-server-response.jstest/js/node/test/parallel/test-http-server-reject-chunked-with-content-length.jstest/js/node/test/parallel/test-http-server-reject-cr-no-lf.jstest/js/node/test/parallel/test-http-server-request-timeout-delayed-body.jstest/js/node/test/parallel/test-http-server-request-timeout-delayed-headers.jstest/js/node/test/parallel/test-http-server-request-timeout-interrupted-body.jstest/js/node/test/parallel/test-http-server-request-timeout-interrupted-headers.jstest/js/node/test/parallel/test-http-server-request-timeout-keepalive.jstest/js/node/test/parallel/test-http-server-request-timeout-pipelining.jstest/js/node/test/parallel/test-http-server-request-timeout-upgrade.jstest/js/node/test/parallel/test-http-server-stale-close.jstest/js/node/test/parallel/test-http-server-unconsume.jstest/js/node/test/parallel/test-http-server.jstest/js/node/test/parallel/test-http-set-cookies.jstest/js/node/test/parallel/test-http-set-header-chain.jstest/js/node/test/parallel/test-http-set-timeout-server.jstest/js/node/test/parallel/test-http-set-timeout.jstest/js/node/test/parallel/test-http-set-trailers.jstest/js/node/test/parallel/test-http-socket-encoding-error.jstest/js/node/test/parallel/test-http-status-code.jstest/js/node/test/parallel/test-http-status-message.jstest/js/node/test/parallel/test-http-timeout-overflow.jstest/js/node/test/parallel/test-http-transfer-encoding-repeated-chunked.jstest/js/node/test/parallel/test-http-unix-socket.jstest/js/node/test/parallel/test-http-upgrade-server-callback.jstest/js/node/test/parallel/test-http-upgrade-server-with-body-and-extras.mjstest/js/node/test/parallel/test-http-upgrade-server-with-body-error.mjstest/js/node/test/parallel/test-http-upgrade-server-with-body.mjstest/js/node/test/parallel/test-http-upgrade-server-with-large-body-unread.mjstest/js/node/test/parallel/test-http-upgrade-server-with-large-body.mjstest/js/node/test/parallel/test-http-url.parse-basic.jstest/js/node/test/parallel/test-http-url.parse-https.request.jstest/js/node/test/parallel/test-http-write-callbacks.jstest/js/node/test/parallel/test-http-zero-length-write.jstest/js/node/test/parallel/test-https-agent-additional-options.jstest/js/node/test/parallel/test-https-agent-keylog.jstest/js/node/test/parallel/test-https-agent-session-eviction.jstest/js/node/test/parallel/test-https-agent-sni.jstest/js/node/test/parallel/test-https-agent.jstest/js/node/test/parallel/test-https-argument-of-creating.jstest/js/node/test/parallel/test-https-autoselectfamily.jstest/js/node/test/parallel/test-https-byteswritten.jstest/js/node/test/parallel/test-https-client-renegotiation-limit.jstest/js/node/test/parallel/test-https-insecure-parse-per-stream.jstest/js/node/test/parallel/test-https-keep-alive-drop-requests.jstest/js/node/test/parallel/test-https-localaddress.jstest/js/node/test/parallel/test-https-max-header-size-per-stream.jstest/js/node/test/parallel/test-https-max-headers-count.jstest/js/node/test/parallel/test-https-options-boolean-check.jstest/js/node/test/parallel/test-https-pfx.jstest/js/node/test/parallel/test-https-resume-after-renew.jstest/js/node/test/parallel/test-https-selfsigned-no-keycertsign-no-crash.jstest/js/node/test/parallel/test-https-server-close-all.jstest/js/node/test/parallel/test-https-server-close-idle.jstest/js/node/test/parallel/test-https-set-timeout-server.jstest/js/node/test/parallel/test-https-strict.jstest/js/node/test/parallel/test-https-timeout-server-2.jstest/js/node/test/parallel/test-https-timeout-server.jstest/js/node/test/parallel/test-https-unix-socket-self-signed.jstest/js/node/test/parallel/test-tls-options-boolean-check.jstest/js/node/test/sequential/test-http-econnrefused.jstest/js/node/test/sequential/test-http-keep-alive-large-write.jstest/js/node/test/sequential/test-http-regr-gh-2928.jstest/js/node/test/sequential/test-http-server-keep-alive-timeout-slow-client-headers.jstest/js/node/test/sequential/test-http-server-keep-alive-timeout-slow-server.jstest/js/node/test/sequential/test-http-server-request-timeouts-mixed.jstest/js/node/test/sequential/test-http2-max-session-memory.jstest/js/node/test/sequential/test-http2-ping-flood.jstest/js/node/test/sequential/test-http2-settings-flood.jstest/js/node/test/sequential/test-http2-timeout-large-write-file.jstest/js/node/test/sequential/test-http2-timeout-large-write.jstest/js/node/test/sequential/test-https-connect-localport.jstest/js/node/test/sequential/test-https-server-keep-alive-timeout.jstest/regression/issue/25190.test.ts
💤 Files with no reviewable changes (5)
- test/js/node/test/parallel/test-http-client-response-timeout.js
- test/js/node/test/parallel/test-http-unix-socket.js
- test/js/node/test/parallel/test-http-client-pipe-end.js
- test/js/node/test/parallel/test-https-unix-socket-self-signed.js
- test/js/node/test/parallel/test-http-client-with-create-connection.js
382f66e to
4d334f5
Compare
4d334f5 to
11d762b
Compare
11d762b to
22246c9
Compare
What this does
Raises
node:http,node:https, andnode:http2compatibility — measured by running Node v26.3.0's owntest/parallel+test/sequentialhttp/https/http2 suites unmodified under Bun — from ~77% to ~94%, and syncs the vendored copies of those suites to v26.3.0.(Counts are upstream Node v26.3.0 test files run with the CI runner config, exit 0 = pass. Every vendored http/https/http2 file passes; this PR adds no
test/expectations.txtentries — it only removes them. The remaining gap is upstream files that are not vendored, listed under Not vendored below.)Fixes #28656 —
http2.createSecureServer({ allowHTTP1: true })answered an HTTPS/1.1 request with an empty reply (curl exit 52): the handshake completed with no ALPN protocol negotiated and the connection closed without a response. On this branch the same repro getsHTTP/1.1 200+ the body (bun 1.3.14 still reproduces the bug). Covered by the synced upstreamtest-http2-https-fallback*.jssuites and theallowHTTP1cases innode-http2.test.js.Server (node:http / node:https)
headersTimeout/requestTimeoutenforcement with Node'sconnectionsCheckingIntervalsweep and raw408reply;keepAliveTimeoutcloses idle keep-alive connections (0= no timeout);server.setTimeout/res.setTimeout/req.setTimeoutarm real per-socket timers, on TLS too. The per-connection idle timer is refreshed in place across the keep-alive cycle (noclearTimeout+setTimeoutper request).'connection'/'secureConnection'at accept/handshake time,'close'emitted whenever the TCP connection closes,closeIdleConnections/closeAllConnectionscounting, socket-error abort, and connection sockets are now realnet.Socketinstances with Node'ssocket.parsersurface (incoming,free(), kOnTimeout slot, freed before'upgrade'/'connect').clientErrorcontract (error reaches the client,bytesParsed/rawPacket, specificHPE_*codes incl.HPE_PAUSED_H2_UPGRADEfor the full 24-byte HTTP/2 preface, premature-EOF, chunked+Content-Length, bare CR, oversized chunk extensions with413/431replies).res.socket === null, ordered flush including buffered1xx, bounded by read backpressure, never advances after a must-close response);maxRequestsPerSocket(503+'dropRequest'); half-close handling. Pipelining lives in the node-http template instantiation, soBun.servekeeps its existing async-pipeline-denied behaviour and fast paths unchanged. Queued responses are bounded by Node's two gates: transport backpressure andstate.outgoingData >= writableHighWaterMark(the bytes buffered across every queued response), so a client that pipelines without reading cannot grow the queue without bound.'end'after handoff), incoming/outgoing trailers (trailer-section size cap honours per-servermaxHeaderSize); the captured trailer section is given the parser's standard post-padding before the 8-byte field-value scanner runs over it, fixing an out-of-bounds read of up to 3 bytes past the section's allocation (ASANheap-buffer-overflowintryConsumeFieldValue, caught in review).s_startstate.calculateLenientFlagsported from Node so clienthttpValidationreaches the response parser; per-serverinsecureHTTPParser/maxHeaderSize(smaller-than-default values are honoured for buffered partial headers too);createServer({ highWaterMark })plumbed to req/res; httpspfx/minVersion/maxVersion/ciphers/ALPN properties (withvalidateObject(options)and the falsy-ALPN default like Node);req.socket.servername/authorized/authorizationError; client/agent fixes (Node-exact messages, domain binding viadomain.runforwarding args/return, parser reentrancy, abort/half-open lifecycle);NODE_TLS_REJECT_UNAUTHORIZEDis now=== "0"only.Connection: closeresponse whose body was still in the native send buffer whenres.end()returned was silently truncated:socket.end()'s native path shut the transport down once the socket's own stream buffer was empty, ignoring the response bytes uWS still held under backpressure, so the FIN overtook them (~1.4 MB of an 8 MiB body reached the client on macOS, where the small loopback send buffer leaves most of the body in userspace; Node v26.3.0 delivers every byte). When the in-flight response still has buffered data the close is now handed to uWS —HTTP_CONNECTION_CLOSEmakes its drain path shut the socket down right after the last byte flushes, the same sequencing Node gets fromdestroySoon(). Three regression tests innode-http-backpressure.test.ts(client-requested close, server-setConnection: close, one-shotres.end(body)), each failing without the fix.node:http2
maxSessionMemorybyte-exact,maxSessionInvalidFrames, PING/SETTINGS-ACK floods).customSettings/remoteCustomSettings,enableConnectProtocol,strictSingleValueFields; settings parsing is transactional and the wire SETTINGS frame carries only the current call's keys whilelocalSettings.customSettingsstays cumulative like Node); the'localSettings'event reports the values the ACK actually acknowledged._writecallback completes asynchronously sowrite()returnsfalsepast the highWaterMark and'drain'fires; over a native socket the deferral isprocess.nextTick, over a JS-side socket /duplexPairit issetImmediateso JS-side delivery has run first);client.close()waits for outstanding SETTINGS ACKs like Node'skMaybeDestroyhasPendingData()check.kTimeoutwith write-progress suppression (a buffer that drained to zero between fires counts as progress),respondWithFile/respondWithFDfd ownership and delivery, AsyncLocalStorage context on client streams, request queueing before connect, per-requestAbortSignal,performServerHandshake, allowHTTP1 fallback headers, Node-exact error wording.session.ping()matches Node: throwsERR_HTTP2_PING_LENGTHfor a non-8-byte payload,validateFunction(callback), returnsfalse(callback getsERR_HTTP2_PING_CANCEL) atmaxOutstandingPings, and each call is anAsyncResource('HTTP2PING').client.request()validates option types before checking session state (Node order), andstream.close(NaN)rejects like Node's default-parameter form. The server rejects an inbound:protocolpseudo-header when its localenableConnectProtocolis0(RFC 8441 §4, like nghttp2 — the request never reaches'stream').http2.server.stream.closechannel now publishes withclosed=true && destroyed=false && rstCode=0for the non-error path anddestroyed=truefor the error path, matching node'sonStreamClose/_destroysplit).request()was called even when that context is empty: the "never captured" default is a sentinel now, so a stream requested outside anyAsyncLocalStoragescope observes an empty store in'response'/'data'/'end'instead of the session's connect-time store (caught in review; new regression test).node:net — fatal write errors and connect semantics
macOS (kqueue) could truncate a hung-up socket's stream:
EV_EOFis reported on the same readable event as a connection's final data, and the event handler honored it even when its read loop had stopped early (short read, repeat budget, or the data callback pausing the socket), ending and closing the socket with bytes still queued in the kernel. Atls.connect()client whose peer didend(big); destroySoon()observed'end'short of the payload — the darwin-aarch64-only CI failure an earlier draft had quarantined. Now a hung-up event is drained torecv() == 0/EAGAINfirst (matching Linux, where the EOF is only ever discovered byrecv() == 0); a hard read error after that drain has delivered data is reported as the end-of-stream the FIN already announced (an RST behind a FIN — Node's reader also stops at the FIN) while a pure RST still reports its error; and the EOF is deferred for a socket the data callback paused mid-burst. Deterministic regression test;test-tls-client-destroy-soon.jsis un-quarantined and the two recv-short TLS decode tests that failed on macOS now pass.A
send(2)that fails after the peer reset the connection now reaches JS. The errno travels usockets →write_check_error→$write(negative errno;-1stays the legacy closed/shutdown sentinel) → node:net fails the pending write callback exactly like Node'sonWriteComplete(lib/internal/stream_base_commons.js#L81-L92). A fatal flush of natively-buffered data (write already acknowledged to JS) is surfaced from the writable dispatch through the socket's'error'handler with the errno-derived code.ENOBUFSstays transient and macOSEPROTOTYPEis reported asECONNRESET, matching libuv'suv__try_write.This replaces an earlier branch of this PR that synthesized
read ECONNRESETfrom a duplicate EOF dispatch. Node treats a repeatedUV_EOFas a no-op, and that synthesized path destroyed healthy sockets mid-read (thetest-net-write-slowCI timeout). With the real mechanism in place,test-http2-max-invalid-framesandtest-http2-reset-floodpass deterministically (verified run-for-run against the Node v26.3.0 binary),test-net-write-slowis clean under 8× parallel load, anddouble-connect.test.tsno longer needstest.failing.New regression test:
test/js/node/net/node-net.test.ts"a write after the peer reset the connection fails with a write error".socket.connect()no longer force-resume()s the stream. Like Node'safterConnectit only doesread(0), so data that arrives before a'data'listener is attached stays buffered instead of being emitted to nobody and lost, and a socket the user never reads follows Node's lifecycle: no'end'/auto-destroy while data sits unread, while the handle stops holding the event loop once the peer's FIN arrives (so the process can still exit). The pre-existingtls-syscall-fault"FIN before close_notify drained" test asserted the old auto-flow side effect (await'close'on a never-read client) and was the deterministic debian-x64-asan timeout; it now consumes the client like Node requires. New regression test: "a connected socket is not flowing until the user reads from it".Fixed from review
Bun.servewas silently made lenient. The new "skip empty lines before the request-line" (llhttp'ss_start) went into the shared templated parser with noIsNodeHttpgate, soBun.serveanswered 200 to a request prefixed with\r\n, a bare\n, or a bare\r— main returns 400. Gated to node-http;serve.test.tsnow asserts the 400 for all three prefixes.Hostheader fired a bogusclientErrorwithHPE_INTERNAL. Node never fires'clientError'for it —parserOnIncominganswers the 400 itself — so Bun now replies byte-for-byte with Node.outgoingData >= writableHighWaterMarkgate had no equivalent. With 50k pipelined requests and the head response held open, Node plateaus at 2148 dispatched; Bun climbed past 6385 with RSS following.H2FrameParserleaked on a throwing handler.process.exit()never unwinds — the VM is destructed from inside theexit()call — so a native frame still on the stack (on_native_read) never returns to drop its+1, and neither the cork slot nor the queued auto-flush task is released. The parser stranded at refcount 4 anddeinit()never ran. The self-keepalive refs now use a counted RAII guard, andfinalizereleases the stranded ones while the VM is shutting down. (Reproduces on main 10/10 under ASAN — pre-existing, not a regression.)Workerleaked its loop state:EventLoop::deinit()never reclaimeddeferred_tasks, finalizer-closed listen sockets were only queued for close before the loop was freed, and the HPACK scratch buffer'sthread_localdestructor is not guaranteed to run. (Also pre-existing on main — a worker running a plainnet.createServer()reproduces the orphaned poll 5/5 under ASAN.)socketOnErrorstarts fromconst session = this[kSession]and does nothing when the session is gone; Bun had no destroyed-guard, so a peer RST racing our own teardown re-entereddestroy(error). It also now ignoresECONNRESETonce a GOAWAY has been received, like Node — verified against v26.3.0, where GOAWAY-then-RST yields['goaway','goaway','close']and no error.read ECONNRESETfamily had a single root cause. On graceful session close Bun stopped reading the socket, so the peer's late GOAWAY sat unread in the kernel buffer and the close sent RST instead of FIN. Node'sfinishSessionCloseexplicitlysocket.resume()s on graceful close for exactly this reason (core.js v26.3.0). Reproduced on Linux with the socket fault-injection layer (recv → ECONNRESETafter the GOAWAY exchange — the identical uncaught-error signature) and fixed by matching Node:resume()beforeend()in both session classes.clientError(HPE_INVALID_EOF_STATE) that fires over plain http was silently swallowed over https — along with CONNECT/Upgrade half-open and pipeline-drain-after-FIN. The dispatch is scoped to uWS HTTP server sockets; every other TLS kind synthesizes its JS'end'from the close event and keeps the historical force-close. Covered by a new test that fails on the unfixed build.'timeout'fired mid-transfer (Windows CI's slower loopback hit the gap reliably). Now judged by a monotonic written counter like Node'scallTimeout._storeHeadercounts the serialized header block inoutputData; Bun counted only body chunks, so power-of-two chunks could landoutgoingDataexactly onwritableHighWaterMarkand pause reads one request earlier than Node — hanging a client that pipelines the unblocking request behind the crossing one (test-http-pipeline-socket-parser-typeerror). Headers are now accounted once per queued response.H2FrameParserfix.test/expectations.txt; remaining entries are down to platform-environmental causes (FinalizationRegistry timing on musl, darwin CI routing).socket.bytesWritten, but the JS getter only mirrors the native counter on drain events - which the parser's direct native writes never raise. Now reads the native handle's live counter (with the JS getter as the fallback for duplexPair-backed sessions).finishSessionCloseliterally:end()first, hard destroy onesetImmediatelater - Node's own documented Windows-ECONNRESET avoidance (core.js v26.3.0). The immediatedestroySoon()destroyed before the peer drained our final GOAWAY, turning its close abortive.close_and_detach, severing the JS wrapper before the close could dispatch -http2.connectto a nonexistent server emitted neither'error'nor'close'and grpc-js calls waited out their full deadline on all platforms. The teardown now closes without detaching, and leaves not-yet-established sockets to the connect-error path.EPROTOTYPEno longer kills healthy connections. libuv retries it (RETRY_ON_WRITE_ERROR); Bun renamed it toECONNRESET, which the new fatal-write handling then acted on - spuriously tearing down darwin h2 sessions mid-transfer. Now classified with the transient errors and retried.READABLE|WRITABLEabsolutely, silently resuming reads the application had paused; they now go through one pause-preserving helper.failWriteapplies the same listener policy asSocketEmitEndNT: a failed flush on an orphaned socket (no callback, no'error'listener - an h2 teardown racing the peer's reset) closes quietly instead of surfacing an unhandledwrite ECONNRESET.destroy()is now idempotent, like Node's (if (this.destroyed) return;opens Node's, v26.3.0). A second destroy re-ran the teardown and re-emitted'error'; on the Windows agents a surfaced socket reset had consumed grpc-js'sonce('error')absorber, and the received-GOAWAY handler's second destroy then threw an unhandled "Session closed with error code 8" on every Windows lane. Guarded with a one-shot latch (not thedestroyedgetter, which reads "socket detached" and is set by#onErrorpre-destroy) that releases when a validation-failure destroy throws — pinned by the conformance suite.UV_DISCONNECT— AFD's only signal for a peer FIN with no read outstanding — was never requested, so a client FIN against a half-closed server socket never fired andserver.close()waited forever. Now armed unconditionally, delivered one-shot (AFD re-reports it forever once signaled; a level-triggered re-fire on a paused socket starved the write side), and mapped to the EOF hint only for sockets whose write side is already shut down — unlike kqueue'sEV_EOF, AFD can signal DISCONNECT while data is still in flight, and an unconditional EOF mapping truncated in-flight transfers; for a shut-down socket no data-bearing flow remains, and that is exactly the state that hung.-1as connection death. It now enumerates the peer-gone class (EPIPE/ECONNRESET/ECONNABORTED/ENOTCONN/ETIMEDOUT/ENET*/EHOSTUNREACH+ the matching raw WSA codes); anything unclassified keeps the historical re-buffer behavior instead of killing a live session (macOS returns racy errnos fromsend()on healthy sockets).forceShutdowndestroys server sessions withNGHTTP2_CANCEL, racing a GOAWAY(8) against a client that alreadyclose()d. Node never observes that frame (its socket is torn down first); on the Windows agents it deterministically arrived and destroyed the closing session with an unhandled error. A session that is closed with zero live streams now destroys cleanly on an error GOAWAY; a session that did not initiate close keeps Node's exact throw (verified side-by-side with v26.3.0).process.nextTick, and session destroy's synchronous pass can tear the same stream down first. Re-destroying a listener-less destroyed stream re-emitted the error as an uncaught exception (Windows); skipping all destroyed streams swallowed grpc's terminal status codes on darwin (1 CANCELLEDacross ten suites). The sweep now skips only listener-less destroyed streams — listened ones keep the delivery, de-duped by Node'serrorEmittedsemantics.uv_run's alive-guard skips timer processing and I/O polling when the loop has no ref'd handles, and every uSockets handle isuv_unref'd by design, so a gracefulserver.close()awaiting a half-closed connection's teardown wedged forever once the server's KeepAlive dropped. Three pieces land here, in dependency order:'upgrade'/CONNECT exchange never releasedpending_requests(the tunneled socket's close had no release path), so the server's all-closed promise silently never resolved for such connections. The handoff now marks the response TUNNELED and rides the existing single-firemark_request_as_donepath.ws.close()/terminate()pre-set the closed flag andon_closethen skipped theactive_connectionsdecrement — every server-initiated close leaked the count forever (server.stop()'s promise never resolved; reproducible on current releases) — and nothing re-evaluated server teardown when websockets drained.on_closenow owns the single decrement and notifies the server.server.close()keeps the process alive until connections end), so the Windows loop keeps polling through teardown. Direct loop-layer fixes (always-run tick with a bounded deadline) were tried, CI-validated as unwedging, and deliberately reverted — they change Windows timer punctuality globally; that avenue is documented in Windows: uv_run's alive-guard wedges teardown states with zero ref'd handles (server.close() waits forever class) #34158.EPROTOTYPEon healthy sockets is the natural trigger, previously "handled" by an infinite silent retry). Deterministically reproduced on Linux via fault injection (×8 recovers / ×9 jammed forever). Unclassified errnos now get a bounded retry window (32 consecutive failures, libuv-style) and then surface like peer-gone errors; the h2 fatal check returns to the blanket form; and a second silent-loss bug found in the same audit — node:net's drain-path flush discarding fatal errnos, acking silently truncated streams as flushed — now fails the pending write and emits'error'. Fault-injection regression tests cover burst-recovery and sustained-failure surfacing at both the h2 and net layers.AFD_POLL_ABORTunless readable polling was armed, and masked its report — so an RST against a socket armed for writable+disconnect was structurally invisible (vendored patch fixes request + report). A paused socket then discriminates without violating the pause:MSG_PEEKseparates graceful FIN (deferred until resume, as before) from a reset — including a reset behind buffered data, recovered viaSO_ERROR— which errors immediately like Node's paused sockets (an abandoned, never-resumed socket otherwise never learns its peer died and pins the process forever).test-net-*reset*tests ("reset not surfaced as ECONNRESET") pass on the Windows lanes with theUV_DISCONNECTdelivery fixes — the entries are removed rather than carried.test-http2-ping-flood.jsis dropped rather than shipped red: it was added by this branch's suite sync (never on main — the old expectations machinery removed http2 files from runs entirely), and on windows-11-aarch64 it cannot pass for any runtime: its never-reading flood client cannot observe the teardown reset on Windows (SO_ERRORdoesn't latch a received RST, a zero-bytesend()still succeeds,MSG_PEEKsees only the buffered data, and the one-shot AFD DISCONNECT report is consumed by the FIN that node's ownfinishSessionCloseshape — ported verbatim here — sends first). The flood-detection contract is covered on every lane by the staged twin instead.setUsingNodeHttpCompat(bool)→enableNodeHttpCompat()), three[[maybe_unused]]markers removed, the per-response booleans folded into the flags word, and the repeated byte loads in the header/trailer value loops hoisted into a local.Follow-ups (recorded, not in this PR)
-errno/-1sentinel protocol on the write path with an explicitWriteResult::Fatal(errno)variant, and plumb Windows WSA→errno translation so Windows surfaces fatal writes too.internal_flushcall sites (on_opendeferred flush,end_buffered,flush()), and don't re-buffer a chunk whose send already failed fatally.h2_frame_parser's direct native writes treat a negativewrite_maybe_corkedresult as "wrote 0, re-buffer"; the session should fail the stream instead.clientErrorit implies) is recorded here rather than widening this PR.node:tls(handed to the tls PR); they will be addressed in follow-up batches.Tests
test/js/node/http/node-http-server-timeouts.test.ts: raw-socket probes ofheadersTimeout,requestTimeout,server.setTimeout, andkeepAliveTimeout, including the guard thatrequestTimeoutstops at request completion and never fires while a slow handler is still streaming its response. All six pass on this branch; five of six fail on Bun without this PR (the knobs never fire).structuredClonetests (added here for the 2GiB serialization-buffer growth crash) now use the smallest buffer that still exercises that growth path and treat a host OOM kill of the child (SIGKILL, no output) as an environment skip; any other signal, nonzero exit, or wrong round-trip still fails. Their previous ~5GB peak was OOM-killed on the Linux x64 CI runners.test/expectations.txt: −135 entries, +0. This PR does not quarantine a single test. The eight named-pipe tests that cannot run on Windows are gated in-file withcommon.skip()and a reason, which is how the upstream suite gates a platform gap — and an improvement on main, where they carryif (common.isWindows) return;, a bare top-level return that reports a false PASS without running an assertion.--expose-internalsharness shim gainedinternal/http,internal/options, andinternal/streams/stateentries so more upstream tests run unmodified.Not vendored (the 46-file gap to 100%)
Upstream files that are deleted, not quarantined — they exercise Node-internal machinery Bun has no equivalent for, and they never passed on main either (main only appeared green because
expectations.txtskipped them):internalBinding('http2')shape / monkey-patching andrequire('internal/http2/util')—binding,client-onconnect-errors,info-headers-errors,respond-errors,respond-nghttperrors,respond-with-fd-errors,server-push-stream-errors,util-headers-list,util-update-options-buffer,options-max-headers-exceeds-nghttp2,socket-proxy,allow-http1-upgrade-wsNODE_DEBUG=http2output,perf_hooks'http2'entries, nghttp2-exact frame padding,async_hooksresource lifecycle events, and adopting a rawnet.Socketinto an h2 server viaemit('connection')test-http2-ping-flood.jsis deleted for a different reason — a platform gap rather than missing internals. Its client floods PINGs and never reads; flood detection, session teardown andserver.close()all complete, but on windows-11-aarch64 the process then cannot exit: a paused socket with the FIN deferred behind buffered data cannot observe the peer's later RST without consuming the stream (AFD's one-shot DISCONNECT report is consumed by the FIN,SO_ERRORdoes not latch a received RST, and a zero-bytesend()still succeeds). Node's own client would sit in the identical state — the test simply never ran on this lane before this PR un-skipped the h2 suite (Node's CI has no windows-11-aarch64). Removed rather than quarantined, per this PR's no-new-expectations.txtrule. The reset-observability work it prompted (AFDABORTsubscription, paused-socket terminal probes, sweep-based escalation) is kept and is what un-quarantines thetest-net-*reset*trio.Known platform gap (unchanged by this PR)
Bun.serve({ unix: ... })does not bind a Windows named pipe, so eight upstream unix-socket http/https testscommon.skip()on Windows. The client side already works vianet.connect. Not a regression — main hides the same gap behind a barereturn.no test proof · iteration 2 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/http/serve.test.ts test/js/bun/net/socket-syscall-fault.test.ts test/js/bun/websocket/websocket-server.test.ts test/js/node/net/net-syscall-fault.test.ts test/js/node/net/node-net.test.ts test/js/node/tls/tls-syscall-fault.test.ts test/js/web/fetch/fetch-leak.test.ts