Skip to content

Replace http.request() with raw TCP/TLS sockets for CONNECT tunneling#663

Draft
jancurn wants to merge 2 commits into
masterfrom
claude/fix-bun-e2e-tests-4p3oy
Draft

Replace http.request() with raw TCP/TLS sockets for CONNECT tunneling#663
jancurn wants to merge 2 commits into
masterfrom
claude/fix-bun-e2e-tests-4p3oy

Conversation

@jancurn

@jancurn jancurn commented May 21, 2026

Copy link
Copy Markdown
Member

Summary

Refactors the upstream proxy CONNECT tunnel implementation to use raw TCP/TLS sockets instead of http.request() with the 'connect' event, making proxy-chain work as a library under Bun 1.3. Rebased onto latest master (pnpm migration, puppeteer 25).

Key Changes

  • Chain handler refactoring (src/chain.ts): Replaced http.request() / https.request() with direct net.createConnection() and tls.connect() calls

    • Manually constructs and sends the CONNECT request as a raw HTTP string, and parses the CONNECT response headers itself
    • The synthetic response object keeps rawHeaders, so tunnelConnectResponded / tunnelConnectFailed subscribers see the same shape as before
    • TLS options configured on httpsAgent (custom ca, client certs, ...) are honored by re-using the agent's stored constructor options for the direct connection
    • Sidesteps two Bun 1.3 quirks: rejection of RFC 7230 authority-form CONNECT paths like :443 with "fetch() URL is invalid", and silent swallowing of 407/590-class upstream responses
  • Server request routing (src/server.ts): onRequest routes CONNECT requests to onConnect() — Bun delivers CONNECT through the generic 'request' event for some client shapes instead of the dedicated 'connect' event

  • Connection stats on Bun (src/server.ts): getConnectionStats() returns undefined under Bun. Bun does not populate socket.bytesRead/bytesWritten on http sockets, so the numbers would silently read as zero — better no stats than wrong stats. (connectionClosed events consequently carry stats: undefined on Bun.) An earlier revision of this PR tried to reimplement byte counting via passive 'data'/'pipe' listeners; that added per-connection listener overhead on all runtimes for little gain and was dropped in favor of this explicit guard.

  • SSL certificate regeneration (test/e2e/ssl.crt, ssl.key): The previous self-signed test cert expired 2018-11-10. Node tolerated it (rejectUnauthorized: false), but Bun rejects expired certs strictly on the WebSocket path. New cert: sha256, SAN covers localhost, localhost-test, 127.0.0.1, ::1, valid until 2126.

Verification

  • Node 22: full e2e suite produces an identical pass/fail set to the master baseline (2012 passing; the same 174 failures exist on pristine master in the same sandbox — puppeteer/IPv6 environment issues, not caused by this PR)
  • Bun 1.3: unit tests and the compatible e2e subset pass; full-suite pass count improves, with the remainder blocked on upstream Bun http.request client bugs (tracked separately)

https://claude.ai/code/session_01Vn16UCyisJSwEaAoHLSNX6

claude added 2 commits June 12, 2026 14:21
The previous self-signed cert expired 2018-11-10. Node tolerated it
because the tests pass `rejectUnauthorized: false`, but Bun rejects
expired certs strictly on the WebSocket path, failing four e2e tests
with "certificate has expired". New cert: sha256, SAN covers
localhost, localhost-test, 127.0.0.1 and ::1, valid until 2126.
Three changes that make proxy-chain work as a library under Bun 1.3,
inspired by the approach in #649:

1. `chain.ts` no longer tunnels through `http.request().on('connect')`.
   Bun implements `http.request` on top of `fetch()`, which (a) rejects
   RFC 7230 authority-form CONNECT paths like `:443` with "fetch() URL
   is invalid" and (b) swallows 407/590-class upstream responses.
   Instead we open a raw `net`/`tls` socket, write the CONNECT request
   ourselves, parse the response headers, and `unshift` any remaining
   bytes back as tunnel payload. The synthetic response object keeps
   `rawHeaders` so `tunnelConnectResponded`/`tunnelConnectFailed`
   subscribers see the same shape as before. TLS options configured on
   `httpsAgent` (custom `ca`, client certs) are honored by re-using the
   agent's stored constructor options for the direct connection.

2. `Server.onRequest` routes `CONNECT` requests to `onConnect`: Bun
   delivers CONNECT through the generic 'request' event for some client
   shapes instead of the dedicated 'connect' event.

3. `Server.getConnectionStats` returns `undefined` under Bun. Bun does
   not populate `socket.bytesRead`/`bytesWritten` on http sockets, so
   the stats would silently read as zero — better no stats than wrong
   stats. (`connectionClosed` events consequently carry
   `stats: undefined` on Bun.)

Node behavior is unchanged: full e2e suite produces an identical
pass/fail set to master baseline (2012 passing, same 174 pre-existing
environment failures) on Node 22.
@jancurn jancurn force-pushed the claude/fix-bun-e2e-tests-4p3oy branch from ea65934 to e76b96b Compare June 12, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants