Skip to content

node:http/https/http2: raise Node v26.3.0 compat to ~93% (timeouts, pipelining, clientError, upgrade/CONNECT/trailers, h2 session errors, limits and flow control, net.Socket server sockets) and sync the upstream suites#32488

Draft
cirospaciari wants to merge 11 commits into
mainfrom
claude/node-http-http2-compat

Conversation

@cirospaciari

@cirospaciari cirospaciari commented Jun 18, 2026

Copy link
Copy Markdown
Member

What this does

Raises node:http, node:https, and node:http2 compatibility — measured by running Node v26.3.0's own test/parallel + test/sequential http/https/http2 suites unmodified under Bun — from ~77% to ~93%, and syncs the vendored copies of those suites to v26.3.0.

module before after
node:http 319/405 (78.8%) 378/405 (93.3%)
node:https 45/65 (69.2%) 59/65 (90.8%)
node:http2 215/278 (77.3%) 257/278 (92.4%)
total 579/748 (77.4%) 694/748 (92.8%)

(Counts are upstream Node v26.3.0 test files run with the CI runner config, exit 0 = pass, Linux x64 debug builds; three of the remaining failures pass standalone and only time out under parallel debug load. The failing remainder is listed in test/expectations.txt.)

Server (node:http / node:https)

  • headersTimeout / requestTimeout enforcement with Node's connectionsCheckingInterval sweep and raw 408 reply; keepAliveTimeout closes idle keep-alive connections (0 = no timeout); server.setTimeout / res.setTimeout / req.setTimeout arm real per-socket timers, on TLS too.
  • Connection lifecycle parity: 'connection'/'secureConnection' at accept/handshake time, 'close' emitted whenever the TCP connection closes, closeIdleConnections/closeAllConnections counting, socket-error abort, and connection sockets are now real net.Socket instances with Node's socket.parser surface (incoming, free(), kOnTimeout slot, freed before 'upgrade'/'connect').
  • Parse errors follow Node's clientError contract (error reaches the client, bytesParsed/rawPacket, specific HPE_* codes incl. HPE_PAUSED_H2_UPGRADE for an HTTP/2 preface, premature-EOF, chunked+Content-Length, bare CR, oversized chunk extensions with 413/431 replies).
  • HTTP/1.1 pipelining (queued responses with res.socket === null, ordered flush, bounded by read backpressure), maxRequestsPerSocket (503 + 'dropRequest'), half-close handling.
  • Upgrade-with-body, CONNECT tunnels (raw data + 'end' after handoff), incoming/outgoing trailers.
  • Strict request parsing only for the node compat layer (llhttp-style method validation and friends are gated on the node-http flags); Bun.serve keeps its lenient parser and fast paths unchanged.
  • Per-server insecureHTTPParser/maxHeaderSize, createServer({ highWaterMark }) plumbed to req/res, https pfx/minVersion/maxVersion/ciphers/ALPN properties, req.socket.servername/authorized/authorizationError, relaxed (httpValidation) header validation on the client side, plus client/agent fixes (Node-exact messages, domain binding, parser reentrancy, abort/half-open lifecycle).

node:http2

  • nghttp2-style session errors and teardown for protocol violations (bad preface, oversized frames, flow-control violations, HEADERS on closed streams, unsolicited PING ACK, GOAWAY parity), flood/resource limits (maxSessionMemory, maxSessionInvalidFrames, PING/SETTINGS-ACK floods).
  • Flow control: stream windows are only replenished while the JS readable is consuming (a paused reader backpressures the peer), and a response that ends before the request body finished no longer stalls the connection — fixes a real-world server bug covered by a new wire-level conformance test.
  • Settings parity (empty initial SETTINGS, customSettings/remoteCustomSettings, enableConnectProtocol, strictSingleValueFields), Writable parity (write completion on flush, 'drain', writableLength, bufferSize), session idle timer under kTimeout with write-progress suppression, respondWithFile/respondWithFD fd ownership and delivery, AsyncLocalStorage context on client streams, request queueing before connect, per-request AbortSignal, performServerHandshake, allowHTTP1 fallback headers, Node-exact error wording.

Tests

  • All upstream http/https/http2 tests are vendored: 102 added, 85 resynced to v26.3.0 content; test/expectations.txt now lists only the genuinely-failing remainder (65 entries removed in total across the PR — including the old test-stream-pipeline.js skip and the test-http2-pipe.js flake, both fixed here — and 38 added for newly-vendored not-yet-passing tests).
  • The --expose-internals harness shim gained internal/http, internal/options, and internal/streams/state entries so more upstream tests run unmodified.
  • Bun-authored tests updated where they asserted the old divergent behavior (http2 wire error codes, clientError handling, abort-signal error code, TLS option message wording, server-timeout auto-destroy); the protected properties of those tests are unchanged.

Verified

  • Full upstream sweep (numbers above), every removed expectations entry re-verified against the vendored file with the CI runner config.
  • Suites at baseline: test/js/node/http/, test/js/node/http2/ (incl. grpc-js), test/js/node/net/, test/js/node/tls/ (per-file), test/js/bun/http/serve.test.ts (unchanged vs main).

Pre-existing issues found while testing (not addressed here)

  • An intermittent ASAN use-after-destroy in the TLS-over-duplex SSLWrapper/UpgradedDuplex teardown (UpgradedDuplex::on_close drops the wrapper while handle_traffic frames still reference it). Reproduces on unmodified main via a single-process bun test test/js/node/tls/ run; the fix belongs in UpgradedDuplex.rs (defer the wrapper drop like WindowsNamedPipe.rs).
  • test/js/node/tls/renegotiation.test.ts ("renegotiation limit over a duplex socket") fails identically on released Bun in this environment.

Remaining known gaps (tracked in test/expectations.txt)

Mostly tests that exercise Node-internal machinery that has no Bun equivalent (internalBinding('http2') monkey-patching, V8 %natives, TimersList inspection, async_hooks resource events, nghttp2-exact frame counts/padding), plus a handful of behavior gaps (HTTP over user-supplied generic streams, request dump/unconsume semantics, https server keylog/handshakeTimeout/ticket-key callbacks, max-header-size boundary accounting).

…pt, keep-alive idle close, async pipelining, llhttp-strict parsing in node mode, upgrade-with-body, CONNECT tunnels, trailers, per-server parser options, SNI accessor

All new parser/lifecycle behavior is gated on the node:http compat flags so Bun.serve keeps its existing lenient parser and fast paths unchanged.
…ientError parity, pipelined response queue, maxRequestsPerSocket, upgrade/CONNECT handoff, trailers, agent and client fixes, https TLS option forwarding (pfx, min/maxVersion, ALPN props), SNI servername, domain binding, abort and half-open socket lifecycle
…imits, settings parity (empty initial frame, customSettings, enableConnectProtocol), write backpressure with deferred completion, client request queueing and AbortSignal, respondWithFile fd ownership, AsyncLocalStorage context, allowHTTP1 fallback headers, trailers ordering
Vendor 102 missing upstream tests (64 passing now, 38 recorded as not yet passing in expectations.txt), resync 84 drifted vendored files to v26.3.0 content, drop 49 expectations entries that now pass (including the test-stream-pipeline.js skip), and update Bun-authored assertions that relied on the old divergent error codes/messages.
@robobun

robobun commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator
Updated 11:36 PM PT - Jun 18th, 2026

@cirospaciari, your commit 4a12f3e has 3 failures in Build #63457 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32488

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

bun-32488 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 17 issues this PR may fix:

  1. Fastify timeout settings not respected when using Bun runtime with NestJS and Fastify adapter #17287 - PR adds headersTimeout, requestTimeout, keepAliveTimeout which Fastify depends on
  2. node:http: Server.closeAllConnections() shuts down the listening socket #31301 - PR explicitly fixes closeAllConnections() to not shut down the listening socket
  3. node:http keep-alive server drops the next reused request after a Content-Length response finalized by a deferred end() (graceful FIN, no Connection: close) #31889 - PR fixes keep-alive connection reuse and HTTP pipelining queue with ordered flush
  4. Bun does not upgrade and hangs when using node:http #32195 - PR adds CONNECT method support and upgrade body delivery improvements
  5. Bun http.request() emits ECONNRESET after successful HTTP 101 Upgrade response #32222 - PR fixes upgrade body delivery and abort/half-open socket lifecycle
  6. node:http bug in edge case usage of res.addTrailers #26171 - PR adds proper addTrailers implementation with chunked encoding support
  7. HTTP/2 GOAWAY drops in-flight requests instead of allowing them to complete #26719 - PR fixes GOAWAY teardown to allow in-flight streams to complete
  8. HTTP/2 Flow Control Bug in node:http2 - Requests Hang Indefinitely #30342 - PR fixes SETTINGS_INITIAL_WINDOW_SIZE delta calculation per RFC 9113 §6.5.3
  9. http2.createSecureServer({ allowHTTP1: true }) returns empty response over HTTPS #28656 - PR implements allowHTTP1 fallback for http2.createSecureServer
  10. HTTP/1.1 fallback broken for node:http2 secure server (allowHTTP1 ignored, ALPN only advertises h2) #26721 - PR fixes ALPN negotiation to advertise http/1.1 when allowHTTP1 is true
  11. support "allowHTTP1" option in http2 #15419 - PR implements the allowHTTP1 option for HTTP/2 secure servers
  12. Cannot modify default TLS cipher suite #18865 - PR adds ciphers, minVersion, maxVersion, secureProtocol TLS options to node:https
  13. AbortSignal.timeout() is not respected for http.request #31167 - PR adds AbortSignal per-request support and abort lifecycle fixes on the client side
  14. @fastify/http-proxy with HTTP/2 spuriously emits FST_REPLY_FROM_HTTP2_REQUEST_TIMEOUT after idle on Bun #30307 - PR fixes HTTP/2 GOAWAY and session lifecycle to prevent spurious timeouts after idle
  15. node:http2 requests hang indefinitely after idle — regression in Bun 1.3.14 (firebase-admin FCM, production) #31881 - PR fixes HTTP/2 session management and GOAWAY handling that caused hangs after idle
  16. request.headersDistinct is undefined in node:http #24268 - PR adds headersDistinct property on request and response objects
  17. Bun's HTTP/2 server potentially ignores Nginx's HPACK for header compression setting SETTINGS_HEADER_TABLE_SIZE=0 #19152 - PR adds customSettings support including proper SETTINGS_HEADER_TABLE_SIZE handling

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

Fixes #17287
Fixes #31301
Fixes #31889
Fixes #32195
Fixes #32222
Fixes #26171
Fixes #26719
Fixes #30342
Fixes #28656
Fixes #26721
Fixes #15419
Fixes #18865
Fixes #31167
Fixes #30307
Fixes #31881
Fixes #24268
Fixes #19152

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. node:https: add addContext and other tls.Server methods to https.Server #32435 - Also adds TLS server methods (addContext, etc.) to node:https, overlapping with this PR's HTTPS server improvements
  2. node:http: expose Server#_connections and validate getConnections callback #32429 - Implements Server#getConnections, which this PR's connection-counting work also covers
  3. node:http: clamp negative IncomingMessage.setTimeout seconds to 0 #32308 - Fixes negative IncomingMessage.setTimeout clamping, superseded by this PR's timeout rework
  4. http2: give Http2Stream its own per-stream idle timer #30308 - Adds per-stream idle timers to Http2Stream, overlapping with this PR's HTTP/2 timeout improvements
  5. http2: reclaim closed-stream entries from the session streams map #30416 - Reclaims closed-stream entries from h2 session streams map, overlapping with this PR's session management
  6. node:http2: fix allowHTTP1 fallback mangling HTTP/1.1 response headers #28657 - Fixes allowHTTP1 fallback header mangling, which this PR also implements
  7. fix: let clientError handler send response before closing socket #28642 - Fixes clientError handler to send response before closing socket, superseded by this PR's clientError contract
  8. http: support client upgrade event #28828 - Adds client upgrade event support, overlapping with this PR's upgrade/CONNECT work
  9. http: support CONNECT method in node:http client #31574 - Adds CONNECT method support in node:http client, which this PR also implements
  10. node:http: hand off upgrade socket to userland #30664 - Hands off upgrade socket to userland, overlapping with this PR's upgrade support
  11. fix(node:http): make upgrade socket.write() actually send data #28347 - Fixes upgrade socket.write() to actually send data, superseded by this PR's upgrade rework
  12. fix(http): preserve server reference across close() for closeAllConnections() #30505 - Preserves server reference across close() for closeAllConnections(), superseded by this PR's connection management
  13. node:http2: populate internal/http2/util for --expose-internals tests [1tpjlb] #29825 - Populates internal/http2/util for tests, overlapping with this PR's HTTP/2 improvements

🤖 Generated with Claude Code

…nel data loss, h2 destroy stream errors

- node:http server: a client FIN no longer tears the connection down while
  pipelined responses are still queued behind the in-flight one; the loop-level
  force-close is disabled for node compat sockets and the connection is shut
  down once the pipeline drains (test-http-server.js). With nothing queued the
  FIN still closes the connection right away, like Node's socket.end().
- node:http upgrade with a body: reading the request no longer flips the raw
  socket into flowing mode, so tunnel bytes arriving before the 'upgrade'
  listener attaches its own 'data' handler are buffered instead of discarded
  (test-http-upgrade-server-with-body*.mjs timed out on optimized builds).
- node:http2: session.destroy(code) only surfaces the session error on streams
  that have an 'error' listener, matching the existing emitStreamErrorNT guard;
  grpc-js forceShutdown no longer produces unhandled errors.
- test-http-should-emit-timeout-event*.ts: requests that already completed do
  not get a 'timeout' event (Node semantics), so the test now uses an
  incomplete request and destroys the socket after the assertion.
- 25190.test.ts: drain the server's data so the socket can emit 'close'.
- expectations: skip the common.PIPE http tests on Windows (named-pipe listen
  unsupported), the flood/teardown http2 tests that only fail on Windows
  runners, and test-http2-pipe (server-side flow control stalls under reader
  backpressure when the response ended early; needs a native fix).
… close, net.Socket-based connection sockets, highWaterMark plumbing, HPE_PAUSED_H2_UPGRADE, relaxed header validation (client half), more expose-internals shim entries
… idle timer under kTimeout with write-progress suppression, respondWithFD bypasses the user-facing writable, strictSingleValueFields, deferred stream destroy when the request body is still buffered
@cirospaciari cirospaciari changed the title node:http/https/http2: raise Node v26.3.0 compat to 90%+ (timeouts, pipelining, clientError, upgrade/CONNECT/trailers, h2 session errors and limits) and sync the upstream suites node:http/https/http2: raise Node v26.3.0 compat to ~93% (timeouts, pipelining, clientError, upgrade/CONNECT/trailers, h2 session errors, limits and flow control, net.Socket server sockets) and sync the upstream suites Jun 18, 2026
…write-file as failing on Windows

Both pass on POSIX; on Windows the file-pipe into the request stream does not
complete and the session timeout fires during a large respondWithFD transfer.
Tracked as Windows follow-ups.
…eHeaders option, handle.close callback contract

A response that finishes before the request body has arrived no longer cuts the
request off: remaining body bytes and trailers keep flowing and 'end' fires at
real message completion (dumping only discards data). createServer's
uniqueHeaders option joins listed headers on one line, and net handles pass a
completion callback to close() like Node's handle contract.
It made requests on terminal paths (rejected smuggled bodies, empty optimized
requests) wait for a message completion that never comes and trips a native
assertion; restore the previous end-of-response behavior and the raw-headers
expectations entry until a refined version lands.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants