Skip to content

test(websocket): record the CONNECT request and tighten the assertions in the proxy tests - #40624

Merged
Jarred-Sumner merged 3 commits into
mainfrom
farm/37d89a2b/websocket-proxy-test-assertions
Aug 27, 2026
Merged

Jarred-Sumner merged 3 commits into
mainfrom
farm/37d89a2b/websocket-proxy-test-assertions

Conversation

@robobun

@robobun robobun commented Aug 27, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • websocket-proxy.test.ts and its twin test/js/first_party/ws/ws-proxy.test.ts check outcomes loosely: toContain on the message list, "some error fired" on the failure paths, no check that a proxy header reached the proxy, no expect() in the ping regression test, and NO_PROXY children that exit 0 or 1.
  • No test checks that the proxy was used. With this container's ambient NO_PROXY=localhost,127.0.0.1,..., 16 of the 19 twin tests pass with the proxy bypassed and the other 3 fail.
  • The file was reported as a 163 s test on darwin aarch64 (build 106656). The runner log shows 199 ms there, and 117 to 208 ms across the last five main builds. No speed change is needed.

Fix

  • proxy-test-utils.ts exposes the CONNECT request it already parses through an onConnectRequest hook, folds the TLS proxy into createConnectProxy({ tls: true }), and exports startRecordingProxy (port, requests, connection count), connectRequest, startEchoServer and the client event helpers.
  • Every tunneling test starts its own recording proxy and asserts the CONNECT request (target, headers, Proxy-Authorization) next to one exact event list: ["connected", message, { code: 1000, reason: "", wasClean: true }]. Failure paths assert the error message and close event (1006 "Proxy connection failed", 1015 "TLS handshake failed") and close an unexpected open at once.
  • The NO_PROXY children print their events. The test compares stdout, stderr, exit code and the proxy's connection count (0 bypassed, 1 used). The twin clears the ambient NO_PROXY, and "explicit proxy wins" checks that the agent's proxy saw no connection.
  • Verified: bun bd test on both files and the two other importers of the helper, 57 pass. Release: 0.27 s for both files (0.22 s + 0.17 s before). Every new assertion fails under a mutation of its input (details below).

Background

  • createConnectProxy is a Node net (or tls) server that parses one CONNECT request, answers 407, 403 or 200, and pipes the client to the target. The hook runs after the parse and before the answer.
  • NO_PROXY also applies to an explicit proxy: option, and bun reads no_proxy before NO_PROXY (src/dotenv/env_loader.rs:374). test: isolate proxy tests from ambient proxy environment variables #37439 moves the clearing into the harness. This PR keeps the module-level clearing in both files until it lands.
Notes

The 163 s figure. Buildkite build 106656, job :darwin: any aarch64 - test-bun (01a04234-72d1): the runner log has the file at [299/3162], Ran 38 tests across 1 file. [199.00ms], and the Buildkite timestamps around it span 206 ms. The other darwin aarch64 job of that build did not run the file. Darwin x64 in the same build: 221 ms. Darwin aarch64 in the last five main builds: 106385 185 ms, 106512 208 ms, 106596 119 ms, 106656 199 ms, 106692 117 ms. The per-test sum of the file is about 0.16 s, so the reported number is most likely the junit suite time read in the wrong unit.

Local timing (this container). Release (USE_SYSTEM_BUN=1 bun test): websocket-proxy 0.24 to 0.39 s before, 0.21 to 0.28 s after; ws-proxy 0.16 to 0.18 s before; both files together 0.26 to 0.28 s after (5 runs). Debug (bun bd test): websocket-proxy 6.1 to 6.4 s before, 6.0 s after; ws-proxy 5.3 s after; both together with the CI env (BUN_GARBAGE_COLLECTOR_LEVEL=1 BUN_JSC_randomIntegrityAuditRate=1.0) 8.5 s, 3 runs. About 2.7 s of the debug time of websocket-proxy is module load (https-proxy-agent, ws, docker/index.ts) plus beforeAll. The slowest tests are the four NO_PROXY children (0.3 s each in debug, they already run concurrently). Nothing else is converted to concurrent: the TLS tunnel regression tests depend on event loop ordering (the setImmediate spin and the held burst), and there is nothing to gain on a 0.2 s file.

Mutations checked against the new assertions (each run on a copy of the file with the release binary, all fail in under 15 ms):

  • client sends no custom header: ws:// through proxy with custom headers fails on the recorded CONNECT request.
  • NO_PROXY=other.host.com in a "direct" case: fails on stdout and proxyConnections: 1.
  • ambient no_proxy=127.0.0.1 in the "proxied" case: fails on stdout and proxyConnections: 0.
  • server never pings: pongs: 0.
  • right credentials in proxy wrong credentials returns error, the CA given in fails without CA certificate (both files): fail with the open and clean close in the event list. Before this change the same mutation hung until the 5 s timeout.
  • twin, explicit proxy option takes precedence over agent with the explicit option pointing at the agent's proxy: fails on explicitRequests: [].
  • twin, process-level no_proxy=127.0.0.1 left in place: 13 of 19 tests fail on requests: [] (before this change all 16 through-proxy tests passed that way).

Scope. Three test files, no runtime change. createTLSConnectProxy is gone: its only callers were the two rewritten files, and createConnectProxy({ tls: true }) replaces it. The Docker Squid section of websocket-proxy.test.ts is untouched (it cannot run here). The constructor checks in both proxy API sections now use port 1 (nothing needs to listen: close() follows at once) instead of the shared fixtures, which are gone. tsc -p test/tsconfig.json reports the same classes of errors on these files before and after (the test tsconfig resolves the DOM WebSocket constructor and @types/ws, so every proxy: option already errors on main; the helper's socket.on("data") Buffer typing is unchanged).

Review. A self-review of the first revision found the recorder in the wrong layer (a second CONNECT parser in the test file, unusable by the twin and by the TLS proxy) and three sessions still on the unrecorded shared proxy. This revision moves the recording into the helper, migrates those sessions and the HTTPS proxy tests, and applies the same assertions to the twin.

Open PRs that touch these files: #37439 (proxy env isolation, edits the NO_PROXY env code in both files), #37638 (adds tests after rejects invalid proxy URL), #37504 and #37487 (add tests after the ping test in websocket-proxy.test.ts). The NO_PROXY block and the ping test are rewritten here, so those PRs need a small rebase.


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

passes on PR (with fix)
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/js/first_party/ws/ws-proxy.test.ts' 'test/js/web/websocket/websocket-proxy.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/js/first_party/ws/ws-proxy.test.ts test/js/web/websocket/websocket-proxy.test.ts
bun test v1.4.1 (731aa92da)

test/js/first_party/ws/ws-proxy.test.ts:
(pass) ws package proxy API > accepts proxy option as string (HTTP proxy) [30.17ms]
(pass) ws package proxy API > accepts proxy option as string (HTTPS proxy) [4.89ms]
(pass) ws package proxy API > accepts proxy option with object containing url [2.64ms]
(pass) ws package proxy API > accepts proxy URL with credentials [3.22ms]
(pass) ws package proxy API > can combine proxy with headers and protocols [4.25ms]
(pass) ws package proxy API > rejects invalid proxy URL [4.66ms]
(pass) ws package through HTTP CONNECT proxy > ws:// through HTTP proxy [493.11ms]
(pass) ws package through HTTP CONNECT proxy > ws:// through HTTP proxy with auth [192.90ms]
(pass) ws package through HTTP CONNECT proxy > proxy auth failure returns error [102.68ms]
(pass) ws package through HTTP CONNECT proxy > proxy wrong credentials returns error [85.41ms]
(pass) ws package wss:// through HTTP proxy (TLS tunnel) > wss:// through HTTP proxy [113.43ms]
(pass) ws package through HTTPS proxy (TLS proxy) > ws:// through HTTPS proxy with CA certificate [246.55ms]
(pass) ws package through HTTPS proxy (TLS proxy) > ws:// through HTTPS proxy with rejectUnauthorized: false [130.83ms]
(pass) ws package through HTTPS proxy (TLS proxy) > ws:// through HTTPS proxy fails without CA certificate [92.43ms]
(pass) ws package with HttpsProxyAgent > ws:// through HttpsProxyAgent [133.63ms]
(pass) ws package with HttpsProxyAgent > wss:// through HttpsProxyAgent with rejectUnauthorized [111.82ms]
(pass) ws package with HttpsProxyAgent > HttpsProxyAgent with authentication [101.82ms]
(pass) ws package with HttpsProxyAgent > HttpsProxyAgent with agent.proxy as URL object [116.92ms]
(pass) ws package with HttpsProxyAgent > explicit proxy option takes precedence over agent [113
... (truncated)
Exit: 0
diff hotspot
test/js/first_party/ws/ws-proxy.test.ts       | 606 +++++++-----------------
 test/js/web/websocket/proxy-test-utils.ts     | 210 ++++++---
 test/js/web/websocket/websocket-proxy.test.ts | 641 ++++++++------------------
 3 files changed, 494 insertions(+), 963 deletions(-)

gate history · 2 passed · 0 rejected · iteration 0

evidence per changed file
file                                           reads  edits  tests
test/js/first_party/ws/ws-proxy.test.ts            1      1      0
test/js/web/websocket/proxy-test-utils.ts          2      3      0
test/js/web/websocket/websocket-proxy.test.ts      7     11      0

Assert the exact event sequence of every proxied session (greeting, echo,
clean close) and the exact error message and close code of the failure
paths. Record the CONNECT request each test's proxy receives and check the
proxy headers and credentials the client sent. The NO_PROXY children print
what they observe and the test compares their stdout, stderr, exit code and
the number of connections the proxy saw. The ping regression test counts the
pongs the server receives.
@coderabbitai

coderabbitai Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 24 days. After that, they cost $0.25 per reviewed file.

Or wait 7 minutes for your next included review.

View limit details

Limit details: You’ve used all 5 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9e521964-dde1-474a-8ab2-8d4089e5fc51

📥 Commits

Reviewing files that changed from the base of the PR and between 65362b5 and 0a2091f.

📒 Files selected for processing (3)
  • test/js/first_party/ws/ws-proxy.test.ts
  • test/js/web/websocket/proxy-test-utils.ts
  • test/js/web/websocket/websocket-proxy.test.ts

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

@robobun

robobun commented Aug 27, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: the change is complete at 0a2091f. The self-review findings (recorder in the wrong layer, three sessions still on the unrecorded proxy, the untouched twin) and the one review thread (dead createTLSConnectProxy) are addressed. CI build #106892 is red only on two files this PR does not touch, so the PR is ready for a maintainer.

What the PR does: the CONNECT request is recorded inside proxy-test-utils.ts (an onConnectRequest hook where the proxy already parses it). Every tunneling test in websocket-proxy.test.ts and in the twin test/js/first_party/ws/ws-proxy.test.ts asserts one exact event list plus the request its proxy received. The twin also clears the ambient NO_PROXY, which its 16 through-proxy tests silently depended on.

Measured before the change: websocket-proxy.test.ts takes 117 to 208 ms on the darwin aarch64 lane in the last five main builds (106385, 106512, 106596, 106656, 106692), 0.22 to 0.39 s with the release binary here, and 6.1 s with the debug build (2.7 s of that is module load and beforeAll). The 163 s figure this work started from does not reproduce, so this PR only tightens the assertions.

Verification: bun bd test on both proxy test files and on the two other importers of the helper (websocket-syscall-fault.test.ts, websocket-pause.test.ts) passes. 5 release runs and 3 debug runs with the CI GC settings are green. Every new assertion was checked against a mutated input (listed in the PR body).

CI (builds #106768, #106889, #106892): the jobs red on every retry are debian 13 x64-asan on test/bundler/transpiler/macro-test.test.ts (LeakSanitizer report) and darwin x64 on test/js/web/url/url.test.ts (TypeError: Invalid URL). Both fail the same way on main at the base commit 65362b5 and are reported separately, as is test/cli/run/require-cache.test.ts. The rewritten tests in both files passed on every lane. websocket-proxy.test.ts shows up as flaky because its two Docker Squid tests, which this PR does not change, fail with "Connection ended" on the first attempt and pass on the retry: the squid healthcheck reports ready before squid listens, and the runner schedules a modified file first, inside that window. #37640 fixes the healthcheck. A rerun at this base would hit the same two main breaks, so the PR is not retriggered.

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

LGTM — test-only refactor that strictly strengthens assertions and consolidates boilerplate.

What was reviewed:

  • startRecordingProxy's second connection listener passively observes the same data events as the tunnel handler (broadcast, not consumed) and detaches after \r\n\r\n, so it cannot interfere with tunnel bytes.
  • Ordering assumptions are deterministic: echoed()'s ["connected", msg, close] order holds because the server sends the greeting in open before any echo; the ping test reads pongs only after serverClosed resolves, and the pong frame precedes the close frame on the wire.
  • No assertion was weakened — every original toContain/"some error fired" check is replaced by an exact toEqual on the full event list plus the recorded CONNECT request; failingSession closes an unexpected open so failures diff instead of timing out.
  • NO_PROXY test.each uses thunks so wsPort is read after beforeAll, spreads bunEnv with no_proxy: undefined, drains stdout/stderr/exited concurrently, and each concurrent case owns its own recording proxy port.
Extended reasoning...

Overview

This PR refactors a single test file, test/js/web/websocket/websocket-proxy.test.ts, collapsing ~390 lines of repeated Promise.withResolvers/onopen/onmessage/onclose boilerplate into shared helpers (clientEvents, echoSession, failingSession, echoed, failed, connectRequest, startRecordingProxy). Every rewritten test now asserts an exact ordered event array via .toEqual instead of toContain or "an error fired", and additionally asserts the CONNECT request line and headers the proxy actually received. The four NO_PROXY subprocess tests are folded into one test.each that asserts a combined {stdout, stderr, exitCode, proxyConnections} object. The ping-over-TLS-tunnel regression test now asserts the server received exactly one pong and closed with 1000. No native or shipped code is touched.

Security risks

None. This is a test-only change with no modifications to runtime code, no new dependencies, no network egress beyond the existing local 127.0.0.1 servers, and no credential handling beyond the pre-existing hardcoded proxy_user:proxy_pass test fixture.

Level of scrutiny

Low-to-moderate. Test-only changes to a single file, no CODEOWNERS entry for the path, no outstanding reviewer objections, and the bug hunt exited on dry_streak. The main risks for a test-tightening PR are (a) silently weakening an existing assertion and (b) introducing flakiness via ordering or race assumptions. I checked both: every original assertion is preserved or strengthened, and the ordering assumptions ("connected" before echo, pong before close frame, serverClosed awaited before reading pongs) are backed by TCP ordering and the server's open-before-message sequencing rather than timing.

Other factors

The refactor closely follows the repo's REVIEW.md and test/CLAUDE.md conventions: using disposal registered before assertions, {...bunEnv, no_proxy: undefined} spread, Promise.all on stdout/stderr/exited, test.each for the parameterized matrix, no throw inside event callbacks (all failure paths funnel through close → resolve), and failingSession closes an unexpectedly-opened socket so a broken invariant produces a diff rather than a timeout. The noProxy values in the test.each table are thunks specifically so wsPort (assigned in beforeAll) is read at test time, not table-construction time. The PR description documents mutation testing of each new assertion, which matches the "prove the test fails for the right reason" requirement.

@robobun

robobun commented Aug 27, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:19 AM PT - Aug 27th, 2026

❌ @robobun, your commit 0a2091f has 2 failures in Build #106892 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 40624

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

bun-40624 --bun

…ighten ws-proxy.test.ts too

proxy-test-utils.ts already parses the CONNECT request it answers. It now
exposes that request through an onConnectRequest hook, folds the TLS proxy
into createConnectProxy({ tls: true }) so an HTTPS proxy can record too, and
exports startRecordingProxy, connectRequest, startEchoServer and the client
event helpers for both proxy test files.

websocket-proxy.test.ts starts a recording proxy in every test that tunnels,
including the three that still used the shared unrecorded proxy and the HTTPS
proxy tests, and drops the shared proxy fixtures. ws-proxy.test.ts gets the
same exact event lists and CONNECT request assertions, clears the ambient
NO_PROXY like its twin, and checks that the agent's proxy sees no connection
when an explicit proxy option wins.
@robobun robobun changed the title test(websocket): tighten the assertions in websocket-proxy.test.ts test(websocket): record the CONNECT request and tighten the assertions in the proxy tests Aug 27, 2026
Comment thread test/js/web/websocket/proxy-test-utils.ts Outdated

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

LGTM — the dead createTLSConnectProxy export I flagged was removed in 0a2091f, and grep confirms the remaining importer (websocket-syscall-fault.test.ts) only uses createConnectProxy/startProxy, which are intact.

Checked that folding the TLS proxy into createConnectProxy({ tls }) preserves the original handler behavior (auth path was already absent from the deleted TLS variant, and no caller passes both tls: true and requireAuth). The new clientEvents/failingSession helpers wire close/error to resolve the awaited promise, so failure paths can't hang. The port: 1 literals are constructor-only tests that close() before any dial, so nothing listens or connects there.

Extended reasoning...

Overview

Test-only refactor across three files: proxy-test-utils.ts gains onConnectRequest/tls options on createConnectProxy, a per-test startRecordingProxy disposable, and structured event collectors (clientEvents, echoSession, failingSession, echoed, failed, connectRequest). Both websocket-proxy.test.ts and ws-proxy.test.ts drop file-scoped shared proxies in favor of per-test using recorders and assert a single .toEqual covering the ordered client event stream plus the exact CONNECT request the proxy received. The NO_PROXY subprocess tests collapse into a test.each asserting stdout + connection count. Net ~470 lines removed. No src/ code touched.

Security risks

None. This is test infrastructure only — no runtime code, no auth/crypto/permissions paths. The proxy helper is a test-local net/tls server bound to 127.0.0.1 on port 0.

Level of scrutiny

Moderate for a test refactor: the main risks are (a) breaking other importers of the shared helper and (b) weakening existing coverage. For (a), grep confirms only three importers of proxy-test-utils; the two rewritten in this PR and websocket-syscall-fault.test.ts, which imports only createConnectProxy/startProxy — both still exported with compatible signatures. For (b), the refactor moves from .toContain/sawError booleans to exact .toEqual on ordered event lists and recorded CONNECT requests, which is strictly stronger per REVIEW.md's "assert the strongest invariant" rule. The failingSession helper closes on unexpected open so a mutation that makes a should-fail path succeed surfaces as an assertion diff instead of a timeout.

Other factors

My only prior finding (dead createTLSConnectProxy alias after its last callers were rewired) was addressed by commit 0a2091f; grep on HEAD finds zero references. The PR description's claim that the alias "stays for other importers" is now stale, but the code is what merges. No outstanding third-party CHANGES_REQUESTED reviews in the timeline. The change aligns with the repo's test conventions (per-test using disposal, port: 0, awaiting the close event rather than sleeping, test.each for the NO_PROXY matrix, ambient env cleared and restored).

@Jarred-Sumner
Jarred-Sumner merged commit 47d954e into main Aug 27, 2026
5 of 6 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/37d89a2b/websocket-proxy-test-assertions branch August 27, 2026 22:53
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