Skip to content

http: proxy stress test suite + fix streamed upload and on_writable UAF through tunnel - #32635

Merged
Jarred-Sumner merged 16 commits into
mainfrom
farm/743ae77b/proxy-stress-tests
Jun 23, 2026
Merged

Jarred-Sumner merged 16 commits into
mainfrom
farm/743ae77b/proxy-stress-tests

Conversation

@robobun

@robobun robobun commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

What

Comprehensive stress testing of the HTTP client proxy code paths (fetch and WebSocket, both ProxyTunnel and WebSocketProxyTunnel): 661 tests across 5 new files plus a shared adversarial proxy/origin helper and a subprocess memory-probe fixture.

File Tests Covers
proxy-stress-helpers.ts n/a Adversarial CONNECT + absolute-form proxy (http/https outer) with per-stage RST/kill/trickle/split hooks; adversarial origin with content-length / chunked / close-delimited × identity/gzip/deflate/br/zstd, truncation at arbitrary byte offset, redirect, echo.
proxy-stress-matrix.test.ts 335 {http,https} proxy × {http,https} origin × framing × encoding × body-size × keepalive response matrix; upload matrix across string/Uint8Array/Blob/FormData/ReadableStream/async-iterator; streamed response via getReader(); trickled 1-byte-per-tick downstream; split CONNECT envelope; RFC 9110 §9.3.6 ignored-header handling; hop-by-hop stripping; method matrix; cross-scheme redirects.
proxy-stress-lifecycle.test.ts 93 Proxy RSTs client at every tunnel stage (request-received / upstream-connected / connect-replied / first-client-byte / first-upstream-byte) × both origins × both keepalive; same during upload (string + stream body); proxy drops upstream at every stage; origin RST at 0/10/60/200 response bytes; close-delimited × compression; abort at every stage; abort after headers; abort churn (200× under ASAN); ASAN-only TLS-alert-during-handshake UAF repro.
proxy-stress-errors.test.ts 51 CONNECT 400/403/407/500/502/503/504/301/302; proxy unreachable; upstream unreachable (502 via CONNECT and absolute-form); proxy auth (missing/wrong/correct, URL and header) for all 4 combos; inner-TLS verify (CA match, no-CA fail, checkServerIdentity reject); unsupported scheme (ftp/socks4/socks5/socks5h/ws); h2-capable origin through proxy stays HTTP/1.1.
proxy-stress-concurrent.test.ts 31 32× parallel per combo × keepalive; tunnel reuse (sequential + auth-keyed); 12 origins through one proxy; reject_unauthorized pool gate (lax → strict forces fresh CONNECT); 4× concurrent 4MB echo; idle pooled tunnel receiving stray data is evicted; 12 subprocess memory probes (6 modes × 2 proxy schemes, 300-1200 iterations each, RSS-growth bounded).
proxy-stress-adversarial.test.ts 151 Stacked split-CONNECT × chunked × compression × keepalive; origin status 200-503 × all combos; origin-facing Host/header shape; checkServerIdentity approve/reject/GC-loop; path/query edges; verbose:true; AbortSignal.timeout; interleaved proxy/direct to same origin; CONNECT target shape; WebSocket proxy matrix (ws/wss × http/https proxy) with echo, 256KB binary, RST at CONNECT, 407 without auth, rapid close, wss-via-https-proxy open/close churn under GC.

Full suite runs in ~5 min under debug+ASAN.

Bugs found and fixed

Two pre-existing bugs surfaced by the suite (both reproduce on main without any test-helper changes). Fixed here in src/http/lib.rs.

1. Streamed request body through a CONNECT tunnel never sent

fetch() to an https:// origin through any proxy with a ReadableStream / async-iterator body hangs forever: CONNECT succeeds, inner TLS handshake completes, the Transfer-Encoding: chunked request head is written, then nothing. String/Uint8Array/Blob/FormData bodies are fine; http:// origins (absolute-form, no tunnel) are fine.

Cause (two layers):

  • The RequestStage::ProxyHeaders arm of on_writable computes has_sent_body = self.request_body().is_empty(). For HTTPRequestBody::Stream the bytes buffer is always empty here, so the request jumps straight to RequestStage::Done and the is_streaming_request_body signal path a few lines below never runs. The non-proxy send_initial_request_payload already gates this on matches!(original_request_body, HTTPRequestBody::Bytes(_)).
  • Even once ProxyBody is reached, its Stream case calls flush_stream → write_to_stream_using_buffer → write_to_socket(socket, …), which writes plaintext chunked bytes to the outer proxy socket instead of through the inner TLS session. The Bytes case right next to it correctly routes through ProxyTunnel::write.

Fix: mirror the non-proxy path's Bytes-only has_sent_body check (and matching debug_assert) in the ProxyHeaders arm; in write_to_stream_using_buffer, route through ProxyTunnel::write when self.proxy_tunnel.is_some(), treating WantRead/WantWrite from the inner SSL as backpressure. Encrypted output reaches the outer socket via the existing write_encrypted callback, same as the Bytes path. This also covers the HTTPThread drain loop which calls the same flush_stream.

2. heap-use-after-free in HTTPClient::on_writable when a TLS alert is buffered with the inner handshake flight

READ of size 1 thread T12 (HTTP Client)
  #0 HTTPClient::on_writable::<true, false>    src/http/lib.rs:~2856
  #1 bun_http::proxy_tunnel::on_handshake      src/http/ProxyTunnel.rs:450
freed by:
  AsyncHTTP::on_async_http_callback_raw        src/http/AsyncHTTP.rs:813
  HTTPClient::close_and_fail::<false>
  bun_http::proxy_tunnel::on_close             src/http/ProxyTunnel.rs:577
  SSLWrapper::handle_reading                   src/uws/lib.rs:1053
  SSLWrapper::flush                            src/uws/lib.rs:669
  ProxyTunnel::on_writable::<false>            src/http/ProxyTunnel.rs:740
  HTTPClient::on_writable::<true, false>       src/http/lib.rs:~2850
  bun_http::proxy_tunnel::on_handshake         src/http/ProxyTunnel.rs:450

If the origin closes or sends a TLS alert in the same buffer as its ServerHello flight (origin rejecting client cert, corrupted stream via misbehaving proxy, origin crash), the client's on_handshake → on_writable → proxy.on_writable → SSLWrapper::flush → handle_reading chain processes the alert, fires on_close → close_and_fail, which runs the result callback and frees the ThreadlocalAsyncHTTP embedding *self. Control returns to on_writable, which immediately reads self.state.flags.is_waiting_for_cert_check on freed memory. Same bug class already documented in start_proxy_handshake's comment.

Fix: close_and_fail → terminate_socket synchronously marks the outer socket closed, and the socket handle is owned by the event loop (outlives the client). Check socket.is_closed() immediately after proxy.on_writable() and return before touching self.

Deterministic ASAN repro in proxy-stress-lifecycle.test.ts ("TLS alert in same buffer as inner handshake"): a CONNECT proxy that double-writes every client→upstream byte, making the origin's TLS stack abort with an alert. Looped 20× with ASAN_OPTIONS=…:abort_on_error=1 so the HTTP-thread UAF aborts the subprocess before the main thread's clean exit wins the race.

Verification

bun bd test test/js/bun/http/proxy-stress-*.test.ts
# 661 pass, 0 fail (debug+ASAN, ~5min)

Fail-before (src/ stashed, bun bd):

  • proxy-stress-matrix.test.ts -t "https-origin POST ReadableStream|https-origin POST async-iterator" → 8 fail (hang/timeout)
  • proxy-stress-lifecycle.test.ts -t "TLS alert in same buffer" → 1 fail (subprocess aborts with heap-use-after-free)

Existing test/js/bun/http/proxy.test.ts (48 tests) still passes.

Related

…AF through tunnel

Adds comprehensive stress testing of the HTTP client proxy code paths:
661 tests across 5 new files plus a shared adversarial proxy/origin helper.

  test/js/bun/http/proxy-stress-helpers.ts      adversarial proxy + origin
  test/js/bun/http/proxy-stress-matrix.test.ts  protocol x framing x encoding x body x keepalive
  test/js/bun/http/proxy-stress-lifecycle.test.ts  RST/abort/close at every tunnel stage
  test/js/bun/http/proxy-stress-errors.test.ts  CONNECT failures, auth, unreachable, TLS verify
  test/js/bun/http/proxy-stress-concurrent.test.ts  pool churn + subprocess memory probes
  test/js/bun/http/proxy-stress-adversarial.test.ts  stacked conditions + WebSocket proxy tunnel

The adversarial proxy supports both CONNECT tunneling and absolute-form
forwarding over a plain-TCP or TLS outer socket, with hooks to RST at any
stage, split/trickle bytes, force CONNECT status codes, require auth, and
track per-connection wire state. The adversarial origin shapes responses
(content-length / chunked / close-delimited, all compression encodings)
and can truncate at any byte offset.

Two pre-existing bugs surfaced by the suite and fixed here in
src/http/lib.rs:

1. Streamed request body (ReadableStream / async-iterator) through a
   CONNECT tunnel to an https origin hangs forever. The ProxyHeaders arm
   of on_writable computed has_sent_body from request_body().is_empty(),
   which is always true for HTTPRequestBody::Stream, so the request
   jumped to Done before the stream was ever signalled. Even with that
   fixed, write_to_stream_using_buffer wrote streamed body bytes to the
   outer socket instead of through the inner TLS session. Fixed by
   matching send_initial_request_payload's Bytes-only has_sent_body
   check and routing write_to_stream_using_buffer through
   ProxyTunnel::write when a tunnel is active.

2. heap-use-after-free in HTTPClient::on_writable when a TLS alert is
   buffered alongside the inner handshake ServerHello flight.
   on_handshake -> on_writable -> proxy.on_writable -> SSLWrapper::flush
   -> handle_reading processes the alert, fires on_close ->
   close_and_fail, which frees the ThreadlocalAsyncHTTP embedding *self
   via the result callback; control returns to on_writable and touches
   freed self. Fixed by checking socket.is_closed() after
   proxy.on_writable() before touching self again (same hazard already
   documented in start_proxy_handshake).
@robobun

robobun commented Jun 23, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:10 PM PT - Jun 23rd, 2026

❌ @robobun, your commit 554db3e has 1 failures in Build #64297 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32635

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

bun-32635 --bun

@robobun

robobun commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

Independently reproduced and fixed the on_writable UAF (bug 2 in this PR) on farm/4bb9eabe/proxy-tunnel-on-writable-uaf; arrived at the identical socket.is_closed() guard after proxy.on_writable. Fail-before/pass-after verified under ASAN against current main (c6be834), so the guard still applies cleanly after a rebase. Not opening a separate PR.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Fixes the HTTPClient proxy-tunnel write path in src/http/lib.rs by routing buffered stream bytes through ProxyTunnel::write instead of the outer socket, adding a socket.is_closed() liveness guard in on_writable, and correcting has_sent_body semantics for non-Bytes bodies in the ProxyHeaders stage. Backs the fixes with a new shared adversarial harness (proxy-stress-helpers.ts) and ten new test suites covering matrix, error, protocol, lifecycle, adversarial, and concurrent scenarios.

Changes

Proxy tunnel fixes and stress test coverage

Layer / File(s) Summary
Proxy tunnel write path, liveness guard, and ProxyHeaders body-type fix
src/http/lib.rs
Routes stream-buffer bytes through ProxyTunnel::write with backpressure in write_to_stream_using_buffer; adds an early-return after proxy.on_writable when socket.is_closed(); makes has_sent_body conditional on body type (Bytes vs Stream/Sendfile) and updates the ProxyHeaders debug assertion accordingly.
Adversarial proxy and origin test harness
test/js/bun/http/proxy-stress-helpers.ts
Adds createAdversarialProxy (with lifecycle stage hooks, split CONNECT replies, trickle, RST timing, auth enforcement) and createAdversarialOrigin (configurable status, framing, encoding, echo, RST-after-bytes), plus env-hygiene utilities (clearProxyEnv, restoreProxyEnv, proxyFreeEnv) and shared helpers (laxTls, makeBody, errcode, deadPort, cartesian).
Memory/UAF subprocess fixture
test/js/bun/http/proxy-stress-memory-fixture.ts
Standalone subprocess that creates a local CONNECT proxy and HTTPS origin, runs fetch+optional-abort iterations across multiple modes with a warmup/GC phase, and emits a JSON RSS summary before exiting.
Protocol matrix and upload shape tests
test/js/bun/http/proxy-stress-matrix.test.ts
Exercises combinatorial response matrix (TLS × framing × encoding × body size × keepalive), upload shapes (string/Uint8Array/Blob/FormData/ReadableStream/async-iterator), streamed response via getReader(), trickled bytes, split CONNECT reply, ignored CONNECT headers, hop-by-hop header stripping, HTTP method matrix, and redirect-through-proxy.
Header handling and response consumption tests
test/js/bun/http/proxy-stress-headers.test.ts
Validates preservation of custom response headers, duplicate Set-Cookie handling, Content-Type preservation, 206 Partial Content with Content-Range, decompress: false behavior, CONNECT envelope headers, long request paths, tunnel pool separation, and default decompression (body decoded, Content-Encoding retained).
Error surface and protocol-failure tests
test/js/bun/http/proxy-stress-errors.test.ts
Validates CONNECT non-200 status propagation without origin contact, refused proxy/upstream connection errors, proxy auth (407/403/200), inner-TLS verification (CA match, no-CA failure, checkServerIdentity rejection), unsupported proxy schemes, and HTTP/2 downgrade to HTTP/1.1 through proxy.
Protocol-level edge cases and request/response handling
test/js/bun/http/proxy-stress-protocol.test.ts
Exercises early origin replies during upload, multi-hop redirect chains, large request/response headers, 1xx informational responses, IPv6 literal targets, proxy.headers forwarding, proxy: "", HTTP/1.0 responses, redirect: "manual", redirect: "error", and response consumer APIs (text, arrayBuffer, bytes, blob, json).
Lifecycle teardown and abort stress tests
test/js/bun/http/proxy-stress-lifecycle.test.ts
Covers RST at every proxy stage (client-side, upstream-side), truncated response byte offsets, close-delimited EOF body, abort-at-each-stage polling, abort-after-headers, an ASAN-only TLS-alert subprocess probe, and in-process abort-churn stress.
Adversarial combination and WebSocket tests
test/js/bun/http/proxy-stress-adversarial.test.ts
Exercises stacked hostile CONNECT combinations, origin status matrix, header rewriting, checkServerIdentity re-entry, path/query encoding, verbose mode, AbortSignal.timeout, WebSocket tunneling (echo, large frames, RST, 407, rapid close, wss double-TLS churn), interleaved proxy/direct fetches, and CONNECT target format.
Concurrent stress, tunnel pooling, and memory probe tests
test/js/bun/http/proxy-stress-concurrent.test.ts
Validates parallel request isolation, tunnel reuse/keepalive, auth-hash separation, multi-origin pool churn, rejectUnauthorized pooling gate, 4 MB bidirectional load, idle-tunnel eviction via stray byte injection, and subprocess RSS/UAF probe using the memory fixture.

Possibly related PRs

  • oven-sh/bun#31325: Both PRs update proxy-tunnel write paths through ProxyTunnel::write and inner TLS gating to ensure request payloads traverse the encrypted tunnel rather than bypassing it during the handshake/approval phase.
  • oven-sh/bun#32462: Both PRs update src/http/lib.rs proxy-tunneling write paths (notably on_writable/RequestStage::ProxyHeaders) to prevent stalls when the outer proxy socket is closed by adding early-return guards and liveness checks.

Suggested reviewers

  • Jarred-Sumner
  • alii
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the two main changes: proxy stress tests and fixes for streamed upload and use-after-free bugs in tunnel handling.
Description check ✅ Passed The description comprehensively covers what the PR does, the bugs found/fixed, and verification steps, aligning well with the template requirements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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


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

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. fetch: send streamed request body through CONNECT proxy tunnel #32636 - Fixes the same two bugs: streamed request body through CONNECT proxy tunnel never sent (has_sent_body incorrectly true for Stream bodies in ProxyHeaders), and write_to_stream_using_buffer writing plaintext to outer proxy socket instead of routing through ProxyTunnel::write

🤖 Generated with Claude Code

@robobun

robobun commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

Re the duplicate-detector flag for #32636: that PR is a focused fix for bug 1 (streamed body through tunnel) only, with a single regression test added to proxy.test.ts. This PR fixes both bugs and carries the full stress suite that found them. The src/http/lib.rs changes for bug 1 overlap; whichever lands first, the other rebases cleanly on top.

@robobun

robobun commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

Overlaps with (and supersedes) #32636, which I've closed. The has_sent_body gate and write_to_stream_using_buffer tunnel routing fixes here are identical; this PR additionally carries the on_writable UAF fix and the stress suite.

Branch farm/b7397eca/fix-proxy-tunnel-stream-body has a minimal 4-test variant of the streamed-body repro if useful for bisection.

robobun and others added 2 commits June 23, 2026 07:33
…ects, 1xx, IPv6, HTTP/1.0)

102 additional tests: early server reply (413/400/500) mid-upload
across all proxy/origin combos; 3-5 hop redirect chains mixing http and
https; 512B-16KB response header values; 1xx informational before 200
through tunnel; IPv6 literal origin; many proxy.headers entries;
proxy:'' force-direct; HTTP/1.0 origin; redirect:manual/error; response
body consumed as text/arrayBuffer/bytes/blob/json.

Also drop unused errcode import from proxy-stress-concurrent.test.ts.
Comment thread test/js/bun/http/proxy-stress-lifecycle.test.ts Outdated
Comment thread test/js/bun/http/proxy-stress-concurrent.test.ts
Drop stale design-narration comments and redundant stageHit promise
from the abort-at-each-stage block in proxy-stress-lifecycle.test.ts;
unused imports in proxy-stress-concurrent/matrix were already cleaned
by autofix.ci + the previous commit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/http/lib.rs`:
- Around line 2723-2765: The proxy tunnel write path is missing socket state
validation that exists in the ProxyHeaders path. Before the first
ProxyTunnel::write call that writes from the buffer, add a guard check to return
an error if socket.is_closed() or socket.is_shutdown() is true. This prevents
inner TLS from buffering bytes indefinitely on a dead outer connection, similar
to the protection already implemented in the ProxyHeaders path. Reference the
socket validation pattern used elsewhere in the codebase to ensure consistency.
- Around line 3143-3148: The `original_request_body` match expression in the
`has_sent_body` calculation does not properly reject `Sendfile` requests before
proxy tunneling, allowing them to reach the `ProxyBody::Sendfile` handler which
unconditionally panics. Add a guard before proxy tunneling is enabled (similar
to how `can_offer_h2()` and `can_try_h3_alt_svc()` work) that returns a
recoverable error when `Sendfile` is encountered in a proxy tunnel scenario, or
modify the match expression to explicitly handle `HTTPRequestBody::Sendfile` and
reject it with an appropriate error instead of allowing it to proceed and panic.

In `@test/js/bun/http/proxy-stress-adversarial.test.ts`:
- Around line 322-331: The server is bound to the IPv4 address 127.0.0.1 on line
322, but the fetch request on line 330 uses localhost which can resolve to
either IPv4 or IPv6 depending on the system configuration, causing
inconsistency. Replace localhost in the fetch URL with 127.0.0.1 to match the
server binding address and ensure the fetch request uses the same host that the
server is listening on.

In `@test/js/bun/http/proxy-stress-concurrent.test.ts`:
- Around line 112-115: The current assertions in the proxyTls conditional block
and the HTTP-proxy rejectUnauthorized case (around lines 226-231) are too
lenient and do not enforce connection reuse properly. In the proxyTls case where
expect(proxy.connectCount()).toBeLessThanOrEqual(5) is used, tighten this
assertion to enforce actual connection reuse by reducing the maximum allowed
CONNECT count significantly below five. Similarly, in the rejectUnauthorized
test case around lines 229-231, tighten the assertion to properly enforce that
the strict tunnel is being reused rather than creating a new connection for each
request. The goal is to make these assertions fail if connection pooling
regresses.
- Around line 79-80: Remove the per-test timeout values from the test blocks in
the proxy-stress-concurrent.test.ts file. Specifically, remove the second
argument (30_000 and other timeout values) that specifies the timeout duration
for each test, as Bun already has built-in timeouts and setting explicit
per-test timeouts weakens hang detection. This applies to the test blocks at
lines 79-80, 183, 232, and 273-274 in the file, where you should remove the
timeout parameter while keeping the test name and callback function intact.
- Around line 413-414: The current assertion using toBeGreaterThanOrEqual is too
lenient and allows the test to pass even when all requests fail. Replace the
check on result.completed and result.failed with an exact equality assertion
that completed plus failed equals iterations, then add mode-specific validation
checks below it that verify the expected outcomes for each test mode (complete,
concurrent-32, redirect, and abort modes should each have distinct failure
expectations to ensure abort paths are properly exercised and other modes
maintain expected completion rates).
- Around line 333-350: The test does not actually verify that the parked tunnel
was evicted before making the second request. Before writing the stray bytes to
liveClients, capture close event promises from each client socket so you can
wait for them to complete. Assert that liveClients is not empty and that a
parked tunnel exists before attempting eviction. After writing the bytes, await
all the close promises to ensure the eviction has actually completed before
proceeding to the second fetch request. In the finally block cleanup, destroy
any remaining live sockets to ensure proper resource cleanup.

In `@test/js/bun/http/proxy-stress-errors.test.ts`:
- Around line 247-295: In the first test case, when proxyTls is true, the outer
HTTPS proxy handshake fails before sending the CONNECT request, causing the test
to pass for the wrong reason. Restrict the first test to only run with HTTP
proxies by adding a condition that skips it when proxyTls is true, or provide
separate tls configuration options for the proxy connection versus the inner
origin connection. In the second test case, the checkServerIdentity function
unconditionally rejects all hostnames including the proxy itself. Modify the
checkServerIdentity callback to conditionally return undefined (no error) when
the hostname matches the proxy, and return new Error("pinned") only when it
matches the origin hostname. Additionally, add assertions to both test cases to
verify that the proxy received at least one CONNECT request (using a method like
proxy.connectCount() or similar) to confirm the tunnel was actually attempted
before the inner TLS failure occurred.

In `@test/js/bun/http/proxy-stress-helpers.ts`:
- Around line 55-58: The restoreProxyEnv function currently sets
previously-absent proxy environment variables to empty strings, which leaves
stale empty values that can interfere with subsequent tests. Modify the function
to properly delete environment variables that were originally absent (when
saved[key] is undefined) by setting them to undefined or using delete, rather
than setting them to empty strings, so that undefined variables remain properly
deleted and do not cause cross-test pollution.
- Line 232: The upstream error handler (around line 379-383) and upstream close
handler (around line 385-391) are using socket.destroy() which closes the
connection immediately and can discard the 502 response before it's sent.
Replace the destroy() calls with client.end() to allow responses to flush
gracefully, following the same pattern already demonstrated elsewhere in the
file (see lines 250-251 and 407-412 where client.write() is followed by
client.end()). Additionally, add boolean flags to track whether the client
socket has already been closed, preventing race conditions where both the error
and close handlers might fire and attempt to close the same socket twice, which
causes the upstream refused to 502 error tests to be flaky.

In `@test/js/bun/http/proxy-stress-lifecycle.test.ts`:
- Around line 315-352: Replace the polling mechanism that checks the want
predicate on proxy.connections[0] (the poller async function using setImmediate)
with a stage callback approach. Add a non-mutating stage callback parameter to
the proxy helper that gets invoked when each proxy stage is reached, and drive
the abort from that callback by calling ac.abort() and signalStage() when the
target stage parameter is reached. This ensures the abort happens at the actual
observable proxy stage event rather than relying on polling a connection record
that may never flip to the expected state.

In `@test/js/bun/http/proxy-stress-matrix.test.ts`:
- Around line 229-232: The assertion `expect(chunks).toBeGreaterThanOrEqual(1)`
in the proxy stress matrix test is too weak because a value of 1 is already
guaranteed by the earlier check `got === payload.length`, so this doesn't
actually detect if the body was buffered into a single read as the comment
suggests it should. Change the assertion to `expect(chunks).toBeGreaterThan(1)`
to properly enforce that multiple chunks are received, which will correctly
validate that the stream is not pre-buffering the 128KB body before exposing it
to the stream consumer.
🪄 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: e79a45c0-f6fb-48fe-9cd8-0db0d893198c

📥 Commits

Reviewing files that changed from the base of the PR and between 126e800 and 33f1675.

📒 Files selected for processing (8)
  • src/http/lib.rs
  • test/js/bun/http/proxy-stress-adversarial.test.ts
  • test/js/bun/http/proxy-stress-concurrent.test.ts
  • test/js/bun/http/proxy-stress-errors.test.ts
  • test/js/bun/http/proxy-stress-helpers.ts
  • test/js/bun/http/proxy-stress-lifecycle.test.ts
  • test/js/bun/http/proxy-stress-matrix.test.ts
  • test/js/bun/http/proxy-stress-memory-fixture.ts

Comment thread src/http/lib.rs
Comment thread src/http/lib.rs
Comment thread test/js/bun/http/proxy-stress-adversarial.test.ts Outdated
Comment thread test/js/bun/http/proxy-stress-concurrent.test.ts
Comment thread test/js/bun/http/proxy-stress-concurrent.test.ts
Comment thread test/js/bun/http/proxy-stress-errors.test.ts
Comment thread test/js/bun/http/proxy-stress-helpers.ts
Comment thread test/js/bun/http/proxy-stress-helpers.ts
Comment thread test/js/bun/http/proxy-stress-lifecycle.test.ts
Comment thread test/js/bun/http/proxy-stress-matrix.test.ts Outdated
Comment thread test/js/bun/http/proxy-stress-protocol.test.ts
Comment thread test/js/bun/http/proxy-stress-helpers.ts Outdated
Comment thread test/js/bun/http/proxy-stress-helpers.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 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 `@test/js/bun/http/proxy-stress-protocol.test.ts`:
- Around line 362-378: The test for 'proxy: "" forces direct' does not actually
set up an ambient proxy environment, so it passes regardless of whether proxy:
"" is being honored. To properly test that proxy: "" bypasses ambient proxies,
you need to configure the proxy URL created with createAdversarialProxy as the
ambient proxy in the environment before calling fetch, either by setting it as
an environment variable in an isolated subprocess context or using env isolation
to prevent race conditions with concurrent tests. Then keep the existing
assertion that proxy.connections.length equals 0 to prove the ambient proxy was
actually bypassed by the empty string proxy parameter.
- Around line 258-314: The test assertion on line 309 is too lenient and accepts
"resolved:502" as a valid outcome even though the beforeAll block (lines
262-273) has already confirmed that IPv6 loopback (::1) is available on this
host. Since ::1 is provably listenable, the proxy should successfully dial the
IPv6 origin and return a 200 response rather than failing with a 502. Tighten
the expectation in the contain() call to only accept the "200:v6" outcome, and
if the createAdversarialProxy helper is not properly handling bracketed IPv6
address format (like [::1]), normalize the IPv6 address format in that helper to
ensure correct connection handling.
🪄 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: 49039992-80a3-477c-9239-ab58b5f6fd44

📥 Commits

Reviewing files that changed from the base of the PR and between 33f1675 and fa14253.

📒 Files selected for processing (2)
  • test/js/bun/http/proxy-stress-lifecycle.test.ts
  • test/js/bun/http/proxy-stress-protocol.test.ts

Comment thread test/js/bun/http/proxy-stress-protocol.test.ts Outdated
Comment thread test/js/bun/http/proxy-stress-protocol.test.ts Outdated
robobun and others added 2 commits June 23, 2026 08:06
….test.ts

src/http/lib.rs: guard write_to_stream_using_buffer's tunnel path on
socket.is_closed()/is_shutdown(), matching the ProxyHeaders arm, so
inner TLS writes don't buffer forever on a dead outer socket.

proxy-stress-helpers.ts: strip IPv6 brackets before net.connect
(getaddrinfo rejects '[::1]'); use client.end() instead of destroy()
on upstream error/close so 502 replies flush; drop stale consumer
list from the header comment.

proxy-stress-protocol.test.ts: tighten IPv6 test to assert 200 now
that the helper dials ::1 correctly; recast the proxy:'' test to
document that an empty-string proxy option is currently treated as
absent (fetch.rs get_length > 0 check), not as 'explicitly no proxy'
per the FetchTasklet comment, so ambient env is not overridden.

proxy-stress-errors.test.ts: assert connectCount() in the inner-TLS
verification tests so an outer-proxy TLS failure can't mask them.

proxy-stress-concurrent.test.ts: wait for the parked socket's close
event in the idle-eviction test; tighten memory-probe assertions to
exact iteration count + mode-specific completed/failed expectations;
assert http-proxy reject_unauthorized reuse exactly.

proxy-stress-matrix.test.ts: drop the vacuous chunks>=1 assertion.

proxy-stress-adversarial.test.ts: fetch 127.0.0.1 to match the
never-reply origin's bind address.

proxy-stress-headers.test.ts: 76 new tests covering many response
headers, duplicate Set-Cookie, Content-Type matrix, 206 Content-Range,
decompress:false, CONNECT envelope shape, long URLs, proxy switching,
and Content-Encoding after decompress.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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 `@test/js/bun/http/proxy-stress-headers.test.ts`:
- Around line 289-306: The test "Content-Encoding header after decompress"
verifies the body is decompressed correctly but does not verify that the
Content-Encoding header is actually removed from the response, which contradicts
the stated behavior in the test comments. Add an assertion after the existing
body check that verifies the Content-Encoding header is absent by checking that
res.headers.get("content-encoding") returns a falsy value (null or undefined),
mirroring the inverse assertion pattern used in the decompress:false test case.
🪄 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: 8785cda4-127a-44bd-bdf5-6c7cc8d25103

📥 Commits

Reviewing files that changed from the base of the PR and between fa14253 and d51264b.

📒 Files selected for processing (8)
  • src/http/lib.rs
  • test/js/bun/http/proxy-stress-adversarial.test.ts
  • test/js/bun/http/proxy-stress-concurrent.test.ts
  • test/js/bun/http/proxy-stress-errors.test.ts
  • test/js/bun/http/proxy-stress-headers.test.ts
  • test/js/bun/http/proxy-stress-helpers.ts
  • test/js/bun/http/proxy-stress-matrix.test.ts
  • test/js/bun/http/proxy-stress-protocol.test.ts
💤 Files with no reviewable changes (1)
  • test/js/bun/http/proxy-stress-matrix.test.ts

Comment thread test/js/bun/http/proxy-stress-headers.test.ts
…ress

Bun keeps the Content-Encoding header after transparent decompression
(issue #5668). The comment claimed it was stripped; correct the comment
and add the matching assertion so the tunnel path is verified to match
direct fetch.
Comment thread test/js/bun/http/proxy-stress-headers.test.ts
Comment thread test/js/bun/http/proxy-stress-headers.test.ts
…r comments

The file header listed folding, long header names, and User-Agent as
covered, but none of those are tested in this file. write_proxy_connect
does not send User-Agent on the CONNECT envelope, so drop that from the
section comment as well.
Comment thread test/js/bun/http/proxy-stress-protocol.test.ts Outdated
Comment thread test/js/bun/http/proxy-stress-helpers.ts Outdated
Comment thread test/js/bun/http/proxy-stress-errors.test.ts
Comment thread test/js/bun/http/proxy-stress-adversarial.test.ts
- proxy-stress-helpers.ts: remove unused onConnection/forceUpstream options
  and absoluteFormCount() method (leftovers from abandoned designs)
- proxy-stress-protocol.ts: gate IPv6 tests with describe.skipIf(!isIPv6())
  instead of a bare early return, so hosts without ::1 report SKIPPED
- proxy-stress-errors.ts: make the h2-not-offered test observe the actual
  inner-TLS ALPN result via a raw tls server that echoes socket.alpnProtocol
- proxy-stress-adversarial.ts: tighten interleaved proxy/direct assertions
  (bytesUp unchanged across direct requests; http-proxy connectCount === 1)
Comment thread test/js/bun/http/proxy-stress-concurrent.test.ts Outdated
…ertion

The separate { mode, growth } toEqual with expect.any(Number) could never
fail. Fold mode + the rounded ratio into the threshold check via a
withinBound:true key so they appear in the diff when the leak bound trips.
Comment thread test/js/bun/http/proxy-stress-adversarial.test.ts
Comment thread test/js/bun/http/proxy-stress-lifecycle.test.ts Outdated
robobun added 2 commits June 23, 2026 10:07
… stageReached

- proxy-stress-adversarial.ts: the large binary frame round-trip test now
  rejects on a clean close before the echo arrives, matching the adjacent
  echo test. A clean server close fires onclose (not onerror), so without
  this the test hung to framework timeout on regression.
- proxy-stress-lifecycle.ts: remove the write-only stageReached flag and
  its tautological assertion. await poller resolving is itself proof the
  stage was reached.
Comment thread src/http/lib.rs Outdated
Comment thread test/js/bun/http/proxy-stress-helpers.ts
…m's handler

SSLWrapper::write_data fires trigger_close_callback before returning a
fatal Err, which runs proxy_tunnel::on_close -> close_and_fail and can
synchronously free the HTTPClient via the result callback. Propagating
that Err to write_to_stream's Err arm then dereferences freed memory.

Match the pre-existing ProxyTunnel::write callers in on_writable: on a
non-WantRead/WantWrite Err, return Ok(true) so write_to_stream only
releases the independently-allocated stream buffer and returns without
touching self.

Also guard createAdversarialProxy's onData against re-entry between head
parse and upstream connect, which could double-push a connection record
and orphan the first dial when an absolute-form client sends body bytes
in a separate packet before loopback connect completes.
Comment thread test/js/bun/http/proxy-stress-adversarial.test.ts Outdated
…ally

Matches the cleanup pattern used by every other inline-server test in
the suite (duplicate Set-Cookie, h2 ALPN, 1xx/early-reply/HTTP1.0
origins). Moves the proxy creation inside the try so a failure there
does not leak the listening origin.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All prior review feedback has been addressed and I found nothing new this round, but the src/http/lib.rs changes touch UAF-prone proxy-tunnel lifecycle paths (three separate liveness/Err-propagation fixes around ProxyTunnel::write / SSLWrapper callbacks) that warrant a human sign-off.

Extended reasoning...

Overview

This PR adds a 661-test proxy stress suite (9 new files under test/js/bun/http/, ~3,500 lines) and fixes three bugs in src/http/lib.rs (~100 lines): (1) streamed request bodies through a CONNECT tunnel were never sent because has_sent_body was computed from the empty Stream buffer and write_to_stream_using_buffer wrote plaintext to the outer socket instead of routing through ProxyTunnel::write; (2) a heap-use-after-free in on_writable when a TLS alert is buffered with the inner handshake flight (the socket.is_closed() guard after proxy.on_writable()); (3) a UAF found during this review where the new tunnel branch's fatal Err from ProxyTunnel::write propagated to write_to_stream's handler after trigger_close_callback had already freed *self — fixed by returning Ok(true) to match the sibling callers' pattern.

Security risks

None introduced. The changes are defensive (liveness guards, routing plaintext through the inner TLS instead of the outer socket). The test suite only spins up loopback servers with self-signed certs and does not touch auth, permissions, or untrusted input parsing in production code.

Level of scrutiny

High for the src/http/lib.rs changes. This is production HTTP-client code on the proxy-tunnel write path, dealing with raw pointer dereferencing (proxy_tunnel::raw_as_mut), SSL wrapper callback ordering, and the exact UAF class this PR is fixing — one of the three fixes was surfaced during review, which confirms the area is subtle. The fixes look correct and well-commented, mirror established patterns elsewhere in the file (the let Ok(...) else { return; } idiom at the other two ProxyTunnel::write call sites), and are backed by deterministic ASAN repros, but memory-safety changes of this kind should have a human reviewer verify the lifecycle reasoning.

The test files are lower risk (new coverage only, no behavior changes to existing tests) but are large enough that flakiness on non-Linux CI lanes is worth watching once they land.

Other factors

This PR has been through extensive iterative review: ~12 inline findings from me and ~14 from CodeRabbit, every one addressed or declined with a clear rationale by the author. The UAF guard at line 2918 was independently reproduced on a separate branch. The duplicate PR #32636 (bug 1 only) was closed in favor of this one. CI showed musl build failures on commit 68fa164 (build-bun, not test failures); the most recent commit 440b4de is test-only, so those may be infra-related — worth confirming the latest CI run is green before merge. The Sendfile-through-proxy panic flagged by CodeRabbit is pre-existing on main and intentionally out of scope here.

@robobun

robobun commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

CI status

The diff is green on every proxy-related test across all lanes. Two consecutive builds each had a single unrelated failure on the same darwin-aarch64-14 test lane:

build failing test subsystem
64196 (8ad1ad9) test/js/third_party/grpc-js/test-server.test.ts SIGTRAP node:http2 server
64216 (440b4de) test/js/node/test/parallel/test-tls-client-destroy-soon.js node:tls client (48KB short read on destroySoon)

Neither test uses fetch() or the HTTP client proxy path. The src/http/lib.rs changes in this PR are all gated on self.proxy_tunnel.is_some() / RequestStage::ProxyHeaders, which node:tls and node:http2 never reach. All seven new proxy-stress-*.test.ts files (839 tests) pass on the failing lane in both builds.

All 32 review threads resolved; claude[bot]'s final review found nothing new and recommends human sign-off for the src/http/lib.rs lifecycle changes. Ready for maintainer review.

Comment thread src/http/lib.rs Outdated
Comment on lines +2715 to +2781
// When tunneling through a proxy the stream body must go through
// the inner TLS session (ProxyTunnel::write), not the outer socket:
// writing plaintext to `socket` here would interleave it with the
// tunnel's TLS records and corrupt the stream at the origin.
// WantRead/WantWrite from the inner SSL are treated as backpressure
// so the next onWritable retries the flush. Encrypted bytes reach
// the outer socket via the SSLWrapper's write_encrypted callback,
// same as the ProxyHeaders/ProxyBody::Bytes paths.
if let Some(proxy_ptr) = self.proxy_tunnel.as_ref().map(|p| p.as_ptr()) {
// Same guard as the ProxyHeaders arm: inner TLS writes succeed
// at the SSL layer and buffer on a dead outer socket forever.
if socket.is_closed() || socket.is_shutdown() {
return Err(err!(ConnectionClosed));
}
let proxy = proxy_tunnel::raw_as_mut(proxy_ptr);
let to_send_len = buffer.slice().len();
if to_send_len > 0 {
match ProxyTunnel::write(proxy, buffer.slice()) {
Ok(amount) => {
self.state.request_sent_len += amount;
buffer.cursor += amount;
if amount < to_send_len {
if !data.is_empty() {
let _ = buffer.write(data);
}
return Ok(true);
}
if buffer.is_empty() {
buffer.reset();
}
}
Err(e) if e == err!(WantRead) || e == err!(WantWrite) => {
if !data.is_empty() {
let _ = buffer.write(data);
}
return Ok(true);
}
// A fatal SSL_write error fires trigger_close_callback
// before returning, which runs proxy on_close →
// close_and_fail and may have freed *self via the
// result callback. Match the other ProxyTunnel::write
// callers: bail without touching self. Ok(true) makes
// write_to_stream only release the (independently
// allocated) stream buffer and return.
Err(_) => return Ok(true),
}
}
if !data.is_empty() {
match ProxyTunnel::write(proxy, data) {
Ok(sent) => {
self.state.request_sent_len += sent;
if sent < data.len() {
let _ = buffer.write(&data[sent..]);
return Ok(true);
}
}
Err(e) if e == err!(WantRead) || e == err!(WantWrite) => {
let _ = buffer.write(data);
return Ok(true);
}
// See the matching arm above: on_close already ran.
Err(_) => return Ok(true),
}
}
return Ok(false);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you simplify this code? It's very repetitive. And the comment is mostly unnecessary.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified in 554db3e. Collapsed both match blocks into let Ok(n) = ... else { ...; return Ok(true); } (all Err variants end up as backpressure anyway), dropped the redundant is_empty() guards, and cut the comments down to two short blocks. Net: 67 lines → 39.

Collapse the two match blocks into let-else form. All Err variants
(WantRead, WantWrite, fatal) return Ok(true) as backpressure, so the
distinction is unnecessary at this level. Trim the comments to the
one line that matters for safety.
@Jarred-Sumner
Jarred-Sumner merged commit d34ca02 into main Jun 23, 2026
64 of 77 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/743ae77b/proxy-stress-tests branch June 23, 2026 22:35
yagop added a commit to yagop/node-telegram-bot-api that referenced this pull request Jul 10, 2026
Bun triage confirmed our report (oven-sh/bun#33918) and root-caused it:
with a ReadableStream body through a CONNECT tunnel, the ProxyHeaders
stage treated the empty stream buffer as already-sent and the chunked
plaintext went to the outer proxy socket instead of the TLS session.
Fixed by oven-sh/bun#32635, first in 1.4.0; verified on canary through
the same sandbox proxy that stalled (all four combos pass).

The guard now buffers only proxied Bun < 1.4.0, so it retires itself as
users upgrade. All four branches unit-verified: proxied 1.3.14
(buffered), direct 1.3.14 (streams), proxied 1.4.0 canary (streams),
Node (streams).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@robobun

robobun commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

Heads-up: proxy-stress-headers.test.ts has been flaking ~1% in CI since it landed (single ConnectionRefused on an https-origin case, or a single wrong Content-Type on an http-origin case, across darwin/alpine/debian). #33975 shares the proxy pair across the file's concurrent tests to pin the proxy ports and remove the listen(0) reuse surface.

@robobun

robobun commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

proxy-stress-matrix.test.ts hit the same one-off ConnectionRefused flake on darwin that #33975 fixed for proxy-stress-headers; #33984 applies the shared-proxy-pair pattern there.

@robobun

robobun commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

proxy-stress-protocol.test.ts has been flaking on darwin since this landed (same port-reuse mechanism as the headers/matrix siblings); shared-proxy fix in #33987.

Jarred-Sumner pushed a commit that referenced this pull request Jul 12, 2026
…33975)

## Problem

`test/js/bun/http/proxy-stress-headers.test.ts` has been flaking in CI
since it was added in #32635, roughly once every ~100 runs across darwin
(14/26, aarch64/x64), alpine x64-baseline, and debian x64-baseline
lanes. Always exactly 1 of 76 tests, in two shapes:

**`ConnectionRefused` on an https-origin test** (e.g. [build
71878](https://buildkite.com/bun/bun/builds/71878),
[71453](https://buildkite.com/bun/bun/builds/71453),
[71343](https://buildkite.com/bun/bun/builds/71343),
[71279](https://buildkite.com/bun/bun/builds/71279),
[70843](https://buildkite.com/bun/bun/builds/70843),
[70725](https://buildkite.com/bun/bun/builds/70725),
[70679](https://buildkite.com/bun/bun/builds/70679)):
```
error: Unable to connect. Is the computer able to access the url?
  path: "https://localhost:58938/",
 errno: 0,
  code: "ConnectionRefused"
✗ Content-Type through tunnel > http-proxy → https-origin Content-Type="application/octet-stream"
```

**Wrong Content-Type received on an http-origin test** (e.g. [build
71888](https://buildkite.com/bun/bun/builds/71888),
[70668](https://buildkite.com/bun/bun/builds/70668),
[70454](https://buildkite.com/bun/bun/builds/70454)), always receiving
`text/html; charset=utf-8` (the second entry in the Content-Type
matrix):
```
Expected: "application/x-www-form-urlencoded"
Received: "text/html; charset=utf-8"
✗ Content-Type through tunnel > http-proxy → http-origin Content-Type="application/x-www-form-urlencoded"
```

Both fail on the single CI retry too.

## Cause

76 `test.concurrent` cases each created a fresh adversarial proxy +
origin, for ~150 `listen(0, "127.0.0.1")` calls per file under the test
runner's rolling `max_concurrency` window (20 in release, 5 under ASAN).
As early tests finish and dispose their servers, later tests'
`listen(0)` can be handed a just-freed port while a sibling test is
still mid-dial (the proxy's `net.connect(port, "localhost")` goes
through autoSelectFamily, adding async hops between port capture and
connect).

I could not reproduce the single-failure shape in isolation (150 runs
linux, 105 serial runs on a macOS 26 CI box all clean; running this file
past ~100 back-to-back iterations on macOS hits full ephemeral-port
exhaustion instead, which is mass EADDRNOTAVAIL/502, not the single-test
failure CI sees). Ruled out as causes: the HTTP client's keep-alive pool
(lookup and release are both gated on `!disable_keepalive`, so
`keepalive: false` never touches the pool), the HTTP-thread scratch
buffers (`SHARED_REQUEST_HEADERS_BUF` etc. are filled and consumed
within one callback with no yield), and node:net cross-connection state
(`lookupAndConnectMultiple` builds a fresh per-call context).

## Fix

Share one `{http, https}` proxy pair across the file, created in
`beforeAll` and torn down in `afterAll`. The adversarial proxy is a
stateless forwarder, so one long-lived pair serves every test that does
not inspect `proxy.connections`; the two tests that do ("CONNECT
envelope shape" and "switching proxy") keep per-test proxies. Proxy
ports are now fixed for the file's lifetime, and per-file `listen(0)`
churn drops from ~152 to ~84.

Coverage is unchanged: 76 tests, 1206 expect() calls. All existing
assertions preserved.

## Verification

```
bun bd test test/js/bun/http/proxy-stress-headers.test.ts   # 76 pass, 0 fail (ASAN)
```
150 consecutive runs on linux and 100 on a macOS 26 CI box, all clean.

The sibling `proxy-stress-matrix.test.ts` has the same per-test proxy
pattern (480 tests) but its payload is a constant `makeBody(n, "R")`, so
if the same port-reuse race bites there the assertion cannot distinguish
one origin's response from another's. Left alone for now since it is not
red in CI.

<!-- robobun:evidence:begin -->

---

**[stamp-90s]** gate passed · iteration 0 · 1 files touched

<details><summary>passes on PR (with fix)</summary>

```console
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/js/bun/http/proxy-stress-headers.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/js/bun/http/proxy-stress-headers.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (9033b14)

test/js/bun/http/proxy-stress-headers.test.ts:
(pass) many response headers > http-proxy → http-origin, 5 response headers [1401.23ms]
(pass) many response headers > http-proxy → http-origin, 50 response headers [1391.27ms]
(pass) many response headers > http-proxy → http-origin, 200 response headers [1386.46ms]
(pass) many response headers > http-proxy → https-origin, 5 response headers [1650.19ms]
(pass) many response headers > http-proxy → https-origin, 50 response headers [1662.04ms]
(pass) many response headers > https-proxy → http-origin, 5 response headers [501.52ms]
(pass) many response headers > https-proxy → http-origin, 50 response headers [551.86ms]
(pass) many response headers > http-proxy → https-origin, 200 response headers [1214.01ms]
(pass) many response headers > https-proxy → http-origin, 200 response headers [1412.05ms]
(pass) many response headers > https-proxy → https-origin, 5 response headers [1597.69ms]
(pass) duplicate Set-Cookie through tunnel > http-proxy → http-origin, 3 Set-Cookie headers [750.87ms]
(pass) many response headers > https-proxy → https-origin, 50 response headers [2082.60ms]
(pass) many response headers > https-proxy → https-origin, 200 response headers [2041.62ms]
(pass) duplicate Set-Cookie through tunnel > http-proxy → https-origin, 3 Set-Cookie headers [994.17ms]
(pass) duplicate Set-Cookie through tunnel > https-proxy → http-origin, 3 Set-Cookie headers [844.36ms]
(pass) Content-Type through tunnel > http-proxy → http-origin Content-Type="text/plain" [251.09ms]
(pass) Co
... (truncated)
Exit: 0
```

</details>

<details><summary>diff hotspot</summary>

```
test/js/bun/http/proxy-stress-headers.test.ts | 33 +++++++++++++++++++--------
 1 file changed, 24 insertions(+), 9 deletions(-)
```

</details>

**gate history** · 2 passed · 0 rejected · iteration 0

<details><summary>evidence per changed file</summary>

```
file                                           reads  edits  tests
test/js/bun/http/proxy-stress-headers.test.ts      1      3      0
```

</details>

<!-- robobun:evidence:end -->
Jarred-Sumner pushed a commit that referenced this pull request Jul 12, 2026
…#33987)

## Problem

`test/js/bun/http/proxy-stress-protocol.test.ts` has been flaking on
darwin since it was added in #32635. Always exactly 1 of 102 tests,
across darwin 14 x64 and 26 aarch64:

[build 71945](https://buildkite.com/bun/bun/builds/71945) (darwin 14
x64): the proxy's upstream dial reached an unrelated Express server
instead of the test origin:
```
- "early"
+ "... Error ... Cannot POST / ..."
✗ early reply during upload > https-proxy → http-origin, 413 after 1KB of a 1MB upload
```

[build 71690](https://buildkite.com/bun/bun/builds/71690) (darwin 14
x64): reached a Verdaccio registry from another job on the same box:
```
- "final-5"
+ "... Verdaccio ... window.__VERDACCIO_BASENAME_UI_OPTIONS={...,\"base\":\"http://localhost:58343/\",...}"
✗ multi-hop redirect through proxy > https-proxy, 5-hop [s→h→s→h→s]
```

[build 71696](https://buildkite.com/bun/bun/builds/71696) (darwin 26
aarch64): 200 response from a server that was not the test origin:
```
TypeError: undefined is not an object (evaluating 'origin.requests[0].headers')
✗ large request headers through tunnel > http-proxy → http-origin, request header 512B
```

[build 71727](https://buildkite.com/bun/bun/builds/71727), [build
71795](https://buildkite.com/bun/bun/builds/71795): `ConnectionRefused`
on the origin URL. All failed on the single CI retry.

## Cause

Same mechanism #33975 diagnosed for `proxy-stress-headers.test.ts` and
#33984 for `proxy-stress-matrix.test.ts`: 102 `test.concurrent` cases
each create a fresh adversarial proxy + origin, issuing ~200 `listen(0,
"127.0.0.1")` calls per file under the test runner's rolling concurrency
window. As early tests dispose their servers, a later test's `listen(0)`
can be handed a just-freed port while a sibling is still mid-dial (the
proxy's `net.connect(port, "localhost")` goes through autoSelectFamily,
adding async hops between port capture and connect). On the persistent
darwin runners the `"localhost"` dial can additionally land on an
unrelated IPv6 listener (Verdaccio / Express from another job), which is
why builds 71690 and 71945 received recognisable third-party HTML.

## Fix

Share one `{http, https}` proxy pair from `beforeAll` across the 68
tests that do not inspect `proxy.connections` (early-reply,
large-headers, 1xx, HTTP/1.0, body-consumer matrices). The 32 tests that
assert on the proxy's connection log (multi-hop redirect, IPv6, `many
proxy.headers`, redirect manual/error) keep dedicated proxies. Per-file
`listen(0)` churn drops from ~200 to ~134.

Coverage is unchanged: 102 tests, 508 `expect()` calls, identical to
main. Same approach as the two open sibling PRs.

Introduced by #32635; sibling fixes are #33975 and #33984.

## Verification

```
bun bd test test/js/bun/http/proxy-stress-protocol.test.ts   # 102 pass, 508 expect() (debug+ASAN)
```
15 consecutive debug+ASAN runs and 30 release runs on linux, all clean.

<!-- robobun:evidence:begin -->

---

**[stamp-90s]** gate passed · iteration 0 · 1 files touched

<details><summary>passes on PR (with fix)</summary>

```console
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/js/bun/http/proxy-stress-protocol.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/js/bun/http/proxy-stress-protocol.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (e926c35)

test/js/bun/http/proxy-stress-protocol.test.ts:
(pass) early reply during upload > http-proxy → http-origin, 413 after 1KB of a 1MB upload [818.20ms]
(pass) early reply during upload > http-proxy → http-origin, 400 after 1KB of a 1MB upload [787.76ms]
(pass) early reply during upload > http-proxy → http-origin, 500 after 1KB of a 1MB upload [788.48ms]
(pass) early reply during upload > http-proxy → https-origin, 413 after 1KB of a 1MB upload [988.57ms]
(pass) early reply during upload > http-proxy → https-origin, 400 after 1KB of a 1MB upload [1089.31ms]
(pass) early reply during upload > https-proxy → http-origin, 413 after 1KB of a 1MB upload [522.18ms]
(pass) early reply during upload > https-proxy → http-origin, 400 after 1KB of a 1MB upload [526.23ms]
(pass) early reply during upload > https-proxy → http-origin, 500 after 1KB of a 1MB upload [366.37ms]
(pass) early reply during upload > http-proxy → https-origin, 500 after 1KB of a 1MB upload [687.29ms]
(pass) early reply during upload > https-proxy → https-origin, 413 after 1KB of a 1MB upload [545.75ms]
(pass) early reply during upload > https-proxy → https-origin, 400 after 1KB of a 1MB upload [521.02ms]
(pass) early reply during upload > https-proxy → https-origin, 500 after 1KB of a 1MB upload [520.15ms]
(pass) multi-hop redirect through proxy > http-proxy, 3-hop [h→h→h] [915.91ms]
(pass) multi-hop redirect through proxy > http-proxy, 3-hop [s→s→s] [1297.09ms]
(pass) multi-hop redirect through proxy > https-proxy, 3-hop [h→h→h] [932.18ms]
(pass) multi-ho
... (truncated)
Exit: 0
```

</details>

<details><summary>diff hotspot</summary>

```
test/js/bun/http/proxy-stress-protocol.test.ts | 34 ++++++++++++++++++++------
 1 file changed, 26 insertions(+), 8 deletions(-)
```

</details>

**gate history** · 1 passed · 0 rejected · iteration 0

<details><summary>evidence per changed file</summary>

```
file                                            reads  edits  tests
test/js/bun/http/proxy-stress-protocol.test.ts      1     12      0
```

</details>

<!-- robobun:evidence:end -->
Jarred-Sumner pushed a commit that referenced this pull request Jul 12, 2026
…33984)

## Problem

`test/js/bun/http/proxy-stress-matrix.test.ts` went red on darwin 14
aarch64 in [build 71934](https://buildkite.com/bun/bun/builds/71934):

```
error: Unable to connect. Is the computer able to access the url?
  path: "https://localhost:60036/",
 errno: 0,
  code: "ConnectionRefused"
✗ response matrix > https-proxy → https-origin chunked/deflate 65536B keepalive=true
334 pass / 1 fail
```

The runner additionally labelled it "5 crashes reported", which skipped
the automatic retry; those crashes are the intentional ones from
`run-crash-handler.test.ts` and `native_plugin_test` earlier in the same
shard that the crash-report collector misattributed (the traces carry
`0xDEADBEEF` / `invoked crashByPanic() handler`). The single
ConnectionRefused is the actual failure.

## Cause

Same mechanism #33975 diagnosed for the sibling
`proxy-stress-headers.test.ts`: 335 `test.concurrent` cases each create
a fresh adversarial proxy + origin, so the file issues ~676 `listen(0,
"127.0.0.1")` calls in <1s under the runner's rolling concurrency
window. As early tests dispose their servers, a later test's `listen(0)`
can be handed a just-freed port while a sibling is still mid-dial (the
proxy's `net.connect(port, "localhost")` goes through autoSelectFamily,
adding async hops between port capture and connect). #33975 left this
file alone because it was not yet red in CI; now it is.

## Fix

Share one `{http, https}` proxy pair from `beforeAll` across the 316
tests that use a default stateless proxy (response / upload / stream /
method matrices). Tests that pass non-default proxy options or assert on
the full `proxy.connections` array (trickled, split-CONNECT,
CONNECT-headers, hop-by-hop, redirect) keep dedicated proxies. This
drops per-file `listen(0)` churn from ~676 to ~362.

The response matrix's per-test `proxy.connections` assertions are
preserved: each test owns a unique origin port for the duration of its
`await using` scope, so `ownConnections()` filters the shared proxy's
append-only log by that port (sliced from the index captured before the
fetch) to unambiguously recover this test's record under
`test.concurrent`, and asserts exactly-one-connection / CONNECT-vs-GET /
`http://` target on it as before.

335 tests, 1126 `expect()` calls: identical to main. Introduced by
#32635; related sibling fix is #33975.

## Verification

```
bun bd test test/js/bun/http/proxy-stress-matrix.test.ts                # 335 pass, 1126 expect() (debug+ASAN)
bun bd test test/js/bun/http/proxy-stress-matrix.test.ts -t "trickled"  # 2 pass, 333 filtered, 0 fail
```
10 consecutive release runs on linux, all clean (1126 expect() each).
[Build 71938](https://buildkite.com/bun/bun/builds/71938) ran this file
clean on every darwin lane (14 aarch64 × 2 shards, 14 x64, 26 aarch64 ×
2).

<!-- robobun:evidence:begin -->

---

**[stamp-90s]** gate passed · iteration 1 · 1 files touched

<details><summary>passes on PR (with fix)</summary>

```console
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/js/bun/http/proxy-stress-matrix.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/js/bun/http/proxy-stress-matrix.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (fb9f9ef)

test/js/bun/http/proxy-stress-matrix.test.ts:
(pass) response matrix > http-proxy → http-origin content-length/identity 128B keepalive=false [832.21ms]
(pass) response matrix > http-proxy → http-origin content-length/identity 128B keepalive=true [790.98ms]
(pass) response matrix > http-proxy → http-origin content-length/identity 65536B keepalive=false [790.77ms]
(pass) response matrix > http-proxy → http-origin content-length/identity 65536B keepalive=true [792.29ms]
(pass) response matrix > http-proxy → http-origin content-length/gzip 128B keepalive=false [889.55ms]
(pass) response matrix > http-proxy → http-origin content-length/gzip 128B keepalive=true [390.04ms]
(pass) response matrix > http-proxy → http-origin content-length/gzip 65536B keepalive=false [389.42ms]
(pass) response matrix > http-proxy → http-origin content-length/gzip 65536B keepalive=true [389.57ms]
(pass) response matrix > http-proxy → http-origin content-length/deflate 128B keepalive=false [387.09ms]
(pass) response matrix > http-proxy → http-origin content-length/deflate 128B keepalive=true [335.56ms]
(pass) response matrix > http-proxy → http-origin content-length/deflate 65536B keepalive=false [327.15ms]
(pass) response matrix > http-proxy → http-origin content-length/deflate 65536B keepalive=true [326.00ms]
(pass) response matrix > http-proxy → http-origin content-length/br 128B keepalive=false [325.91ms]
(pass) response matrix > http-proxy → http-origin content-length/br 128B keepalive=true [326.40ms]
(pass) response matrix > http-proxy → http-o
... (truncated)
Exit: 0
```

</details>

<details><summary>diff hotspot</summary>

```
test/js/bun/http/proxy-stress-matrix.test.ts | 44 +++++++++++++++++++++-------
 1 file changed, 33 insertions(+), 11 deletions(-)
```

</details>

**gate history** · 5 passed · 0 rejected · iteration 1

<details><summary>evidence per changed file</summary>

```
file                                          reads  edits  tests
test/js/bun/http/proxy-stress-matrix.test.ts      4      9      0
```

</details>

<!-- robobun:evidence:end -->
dylan-conway pushed a commit that referenced this pull request Jul 13, 2026
…ead of resolving 'localhost' (#34049)

## Problem

`test/js/bun/http/proxy-stress-matrix.test.ts` went red on darwin 26
aarch64 in [build 72283](https://buildkite.com/bun/bun/builds/72283):

```
error: expect(received).toBe(expected)
Expected: 200
Received: 204
✗ method matrix > OPTIONS via https-proxy → http-origin
334 pass / 1 fail
```

The runner tagged it "crash reported" (the intentional crashes from
`run-crash-handler.test.ts` earlier in the shard, as diagnosed in
#33984), which skipped the automatic retry.

## Cause

Nothing in this test file ever emits 204, and the adversarial origin's
`buildResponse` defaults to 200 for every cell of the method matrix. The
204 came from outside the test process.

`createAdversarialOrigin` binds to `127.0.0.1` but hands out a
`http(s)://localhost:${port}` URL (so that Host / SNI /
`checkServerIdentity` assertions see a hostname). The test proxy parses
the absolute-form target, pulls `host = "localhost"`, and dials
`net.connect(port, "localhost")`. On the darwin boxes `localhost`
resolves to `[::1, 127.0.0.1]` with `::1` first, and autoSelectFamily
tries addresses in that order:

```
$ ssh darwin-test-arm64-3 "bun -e '...dns.lookup(\"localhost\",{all:true},...)'"
[{"address":"::1","family":6},{"address":"127.0.0.1","family":4}]
```

IPv4 and IPv6 ephemeral-port spaces are independent, so an unrelated
process on the bare-metal CI host (another buildkite agent, a system
daemon) can be listening on `[::1]:P` at the exact moment our origin
holds `127.0.0.1:P`. Verified directly on darwin-test-arm64-3: when both
`[::1]:P` (returns 204) and `127.0.0.1:P` (returns 200) are bound,
`net.connect(P, "localhost")` connects to `::1` and reads back 204. On
linux the same script reads 200.

This is the second half of the mechanism #33984 ("the proxy's
`net.connect(port, 'localhost')` goes through autoSelectFamily, adding
async hops") reduced but didn't eliminate: #33984 shared the proxy pair
to cut ephemeral-port churn; each test still creates a fresh origin, and
each origin dial still resolves `localhost`. Introduced by #32635.

## Fix

Normalize `host = "localhost"` to `"127.0.0.1"` in
`createAdversarialProxy`'s upstream dial. Every origin in the suite
binds `127.0.0.1`; the IPv6-literal tests in
`proxy-stress-protocol.test.ts` pass `[::1]` explicitly and are
unaffected. The origin URL (and therefore the client's Host header,
CONNECT target, SNI, and the `checkServerIdentity` host argument)
remains `localhost`, so none of the hostname-shape assertions in
`proxy-stress-adversarial.test.ts` / `proxy-stress-headers.test.ts`
change.

## Verification

```
bun bd test test/js/bun/http/proxy-stress-matrix.test.ts        # 335 pass (debug+ASAN)
bun bd test test/js/bun/http/proxy-stress-adversarial.test.ts \
            test/js/bun/http/proxy-stress-headers.test.ts \
            test/js/bun/http/proxy-stress-protocol.test.ts      # 329 pass
bun bd test test/js/bun/http/proxy-stress-errors.test.ts \
            test/js/bun/http/proxy-stress-lifecycle.test.ts \
            test/js/bun/http/proxy-stress-concurrent.test.ts    # 177 pass
```

15 consecutive release runs of `proxy-stress-matrix.test.ts`, all
335/335.

<!-- robobun:evidence:begin -->

---

**no test proof** · iteration 1 · docs-only change; test-proof not
applicable

<!-- robobun:evidence:end -->
@robobun

robobun commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

The deadPort() + test.concurrent pairing in proxy-stress-errors.test.ts has a port-reuse race (a sibling test's listen(0) can grab the just-freed port before the caller dials it); fixed in #35269.

Jarred-Sumner pushed a commit that referenced this pull request Jul 24, 2026
…'t steal it (#35269)

## Problem

`test/js/bun/http/proxy-stress-errors.test.ts` went red in [build
78599](https://buildkite.com/bun/bun/builds/78599) (debian 13 x64),
[build 78501](https://buildkite.com/bun/bun/builds/78501) (debian 13
aarch64), and [build 77839](https://buildkite.com/bun/bun/builds/77839)
(alpine 3.23 x64). Each sighting is a different "upstream unreachable
via proxy" or "proxy authentication" subtest getting the wrong result:

| build | test | expected | got |
|---|---|---|---|
| 77839 | `http-proxy, absolute-form upstream refused → 502` | 502 | 200
|
| 77839 | `https-proxy → http-origin: wrong auth → 403` | origin not
reached | `origin.requests.length == 1` |
| 78501 | `https-proxy, CONNECT upstream refused → 502` | 502 | 407 |
| 78599 | `http-proxy, absolute-form upstream refused → 502` | 502 |
ECONNRESET |

Each build's annotation also carries `1 crashes reported during this
test`; those traces are `panic: Failed to start File Watcher: EAGAIN`
from `src/jsc/hot_reloader.rs`, which this test cannot reach (it spawns
no subprocess, uses no watcher). They are the shard-level
crash-attribution issue already diagnosed in #33984/#34049 and only
serve to suppress the automatic retry, promoting the flake to a hard
fail.

## Cause

`deadPort()` binds `127.0.0.1:0`, closes the server, and returns the
port for the caller to dial expecting `ECONNREFUSED`. The doc comment
says "nothing reuses it in the microseconds before the caller dials",
which is false under `test.concurrent`: the file's 53 concurrent tests
each issue one to three `listen(0, "127.0.0.1")` calls of their own, and
the kernel's ephemeral allocator happily satisfies one of them with the
port `deadPort()` just released. Mechanically:

```
$ bun -e '... oldDeadPort() then 20× listen(0), 500 trials ...'
old deadPort: port grabbed by subsequent listen(0) in 3 / 500 trials
```

That explains every observed value: 200 is a concurrent
`createAdversarialOrigin` answering; 407 is a concurrent
`createAdversarialProxy({ tls: true, auth })` answering (the test
proxy's upstream TCP connect succeeds, it replies 200 Connection
Established, the client's inner TLS handshake completes against the auth
proxy, which then 407s the GET); ECONNRESET is a concurrent TLS server
receiving plaintext absolute-form bytes; and `origin.requests.length ==
1` is this test's origin being another test's "dead" port.

Introduced by #32635 (the file has always paired `deadPort()` with
`test.concurrent`); same family as the ephemeral-port races
#33975/#33984/#34049 fixed in the sibling files.

## Fix

Hold the port as the local side of a live TCP connection for the
lifetime of the test: bound, so `listen(0)` will not be handed it, and
not listening, so `connect()` to it still gets `ECONNREFUSED`.
`deadPort()` now returns a disposable and the three call sites become
`using dead = await deadPort()`.

```
$ bun -e '... newDeadPort() held, 2000× listen(0) ...'
listen(0) collisions with held port out of 2000: 0
connect to held port: error:ECONNREFUSED
```

## Verification

```
# before (release bun, flake-prone subset -t "unreachable|authentication|unsupported proxy scheme")
6 / 200 runs had a failure

# after (same command)
0 / 200 runs had a failure

# full file, 100 release runs: stable
# bun bd (debug+ASAN), 10 runs: 53 pass / 0 fail / 132 expect() each
```

All six sibling `proxy-stress-*.test.ts` files (788 tests) pass
unchanged.

<!-- robobun:evidence:begin -->

---

**[stamp-90s]** gate passed · iteration 1 · 2 files touched

<details><summary>passes on PR (with fix)</summary>

```console
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/js/bun/http/proxy-stress-errors.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/js/bun/http/proxy-stress-errors.test.ts
bun test v1.4.0 (1e135bf)

test/js/bun/http/proxy-stress-errors.test.ts:
(pass) CONNECT failure status > http-proxy CONNECT → 400 is surfaced as-is [608.07ms]
(pass) CONNECT failure status > http-proxy CONNECT → 403 is surfaced as-is [357.19ms]
(pass) CONNECT failure status > http-proxy CONNECT → 407 is surfaced as-is [353.15ms]
(pass) CONNECT failure status > http-proxy CONNECT → 500 is surfaced as-is [443.44ms]
(pass) CONNECT failure status > http-proxy CONNECT → 502 is surfaced as-is [445.88ms]
(pass) CONNECT failure status > http-proxy CONNECT → 503 is surfaced as-is [281.49ms]
(pass) CONNECT failure status > http-proxy CONNECT → 504 is surfaced as-is [279.92ms]
(pass) CONNECT failure status > https-proxy CONNECT → 400 is surfaced as-is [341.27ms]
(pass) CONNECT failure status > https-proxy CONNECT → 403 is surfaced as-is [316.94ms]
(pass) CONNECT failure status > https-proxy CONNECT → 407 is surfaced as-is [316.58ms]
(pass) CONNECT failure status > https-proxy CONNECT → 500 is surfaced as-is [347.12ms]
(pass) CONNECT failure status > https-proxy CONNECT → 502 is surfaced as-is [351.66ms]
(pass) CONNECT failure status > https-proxy CONNECT → 503 is surfaced as-is [386.69ms]
(pass) CONNECT failure status > https-proxy CONNECT → 504 is surfaced as-is [369.56ms]
(pass) CONNECT failure status > CONNECT → 301 with Location is not followed [386.12ms]
(pass) CONNECT failure status > CONNECT → 302 with Location is not followed [575.16ms]
(pass) proxy unreachable > proxy port refused, http origin [449.75ms]
(pass) proxy unreachable > proxy port refused, https origin [437.50ms]
(pass) CONNECT failure status > https-proxy CONNECT → 101 fails even when the request asked to upgrade [614.67ms]
(pass) CONNECT failure status > http-proxy CONNECT → 101 fails even when the request asked to upgrade [718.76ms]

... (truncated)
Exit: 0
```

</details>

<details><summary>diff hotspot</summary>

```
test/js/bun/http/proxy-stress-errors.test.ts | 12 +++----
 test/js/bun/http/proxy-stress-helpers.ts     | 49 +++++++++++++++++++++-------
 2 files changed, 43 insertions(+), 18 deletions(-)
```

</details>

**gate history** · 2 passed · 0 rejected · iteration 1

<details><summary>evidence per changed file</summary>

```
file                                          reads  edits  tests
test/js/bun/http/proxy-stress-errors.test.ts      1      3      0
test/js/bun/http/proxy-stress-helpers.ts          2      4      0
```

</details>

<!-- robobun:evidence:end -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants