Skip to content

http: compare URL scheme case-insensitively for proxy and redirect Location - #35144

Merged
Jarred-Sumner merged 2 commits into
mainfrom
farm/33fe9693/proxy-scheme-case-insensitive
Jul 22, 2026
Merged

Jarred-Sumner merged 2 commits into
mainfrom
farm/33fe9693/proxy-scheme-case-insensitive

Conversation

@robobun

@robobun robobun commented Jul 22, 2026 •

Copy link
Copy Markdown
Collaborator

What

http_proxy=HTTP://host:port (or any scheme not spelled in lowercase) rejected every request through fetch and bun install with UnsupportedProxyProtocol, while the same string passed via fetch(url, { proxy: "HTTP://..." }) worked. Similarly, a server responding with Location: HTTPS://host/... failed the redirect with UnsupportedRedirectProtocol.

Why

RFC 3986 section 3.1 defines the URL scheme as case-insensitive, and both curl and undici's EnvHttpProxyAgent accept the uppercase form. The { proxy } option path goes through the WHATWG URL parser, which lowercases the scheme; the http_proxy / HTTPS_PROXY environment variables are parsed by bun_url::URL::parse, which is a borrowing parser and keeps protocol as a raw slice of the input. The proxy protocol check in HTTPThread and the is_http()/is_https() helpers compared those bytes exactly. The redirect follower slices the scheme out of the raw Location header bytes before WHATWG normalization runs and compared the same way.

Fix

  • bun_url::URL::is_http, is_https, is_s3, is_file, and has_http_like_protocol now compare ASCII case-insensitively.
  • The two inline scheme checks in HTTPThread go through has_http_like_protocol().
  • The two Location scheme comparisons in the redirect follower go through strings::eql_case_insensitive_ascii.

This also fixes get_port_auto() defaulting HTTPS://proxy (no explicit port) to 80 instead of 443, and HTTPClient::is_https() picking the plaintext context for an HTTPS:// proxy.

Verification

$ USE_SYSTEM_BUN=1 bun test test/js/bun/http/proxy.test.ts -t "http_proxy env var scheme"
(fail) http_proxy=HTTP://... is accepted
  error: UnsupportedProxyProtocol fetching "http://127.0.0.1:.../x"
 1 pass / 3 fail

$ USE_SYSTEM_BUN=1 bun test test/js/web/fetch/fetch-redirect.test.ts -t "Location scheme"
(fail) Location: HTTP://...
  error: UnsupportedRedirectProtocol fetching "http://127.0.0.1:.../start"
 0 pass / 3 fail

$ bun bd test test/js/bun/http/proxy.test.ts -t "http_proxy env var scheme"
 4 pass / 0 fail
$ bun bd test test/js/web/fetch/fetch-redirect.test.ts -t "Location scheme"
 3 pass / 0 fail

Full proxy.test.ts (62 tests) and fetch-redirect.test.ts (15 tests) pass.

Related: #16182 (this covers scheme case only; full WHATWG normalization of the proxy env URL is still open)


[review] gate passed · iteration 0 · 5 files touched

fails on main (without fix)
ASAN without fix: 6 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/http/proxy.test.ts test/js/web/fetch/fetch-redirect.test.ts
bun test v1.4.0 (c50a91c20)

test/js/bun/http/proxy.test.ts:
(pass) GET non-TLS proxy -> non-TLS body type undefined [853.99ms]
(pass) POST non-TLS proxy -> non-TLS body type string [836.44ms]
(pass) GET TLS proxy -> non-TLS body type undefined [985.30ms]
(pass) GET non-TLS proxy -> TLS body type undefined [1142.59ms]
(pass) POST non-TLS proxy -> TLS body type string [1177.59ms]
(pass) POST TLS proxy -> non-TLS body type string [525.95ms]
(pass) GET TLS proxy -> TLS body type undefined [752.01ms]
(pass) POST TLS proxy -> TLS body type string [769.62ms]
(pass) proxy can handle redirects with non-TLS server > with empty body #12007 [937.61ms]
(pass) proxy can handle redirects with non-TLS server > with body #12007 [1119.27ms]
(pass) proxy can handle redirects with TLS server > with empty body #12007 [1255.51ms]
(pass) proxy can handle redirects with TLS server > with body #12007 [1104.45ms]
(pass) proxy can handle redirects with non-TLS server > with chunked body #12
... (truncated)

release without fix: 3 failed, 1 skipped
bun test v1.4.0-canary.1 (6930da6d6)

test/js/bun/http/proxy.test.ts:
(pass) POST non-TLS proxy -> non-TLS body type string [33.08ms]
(pass) GET non-TLS proxy -> non-TLS body type undefined [33.34ms]
(pass) POST TLS proxy -> non-TLS body type string [38.45ms]
(pass) GET TLS proxy -> non-TLS body type undefined [38.48ms]
(pass) POST non-TLS proxy -> TLS body type string [41.92ms]
(pass) GET non-TLS proxy -> TLS body type undefined [44.32ms]
(pass) GET TLS proxy -> TLS body type undefined [49.48ms]
(pass) POST TLS proxy -> TLS body type string [49.48ms]
(pass) proxy can handle redirects with non-TLS server > with empty body #12007 [52.35ms]
(pass) proxy can handle redirects with non-TLS server > with body #12007 [53.21ms]
(pass) proxy can handle redirects with TLS server > with body #12007 [56.46ms]
(pass) proxy can handle redirects with TLS server > with empty body #12007 [58.78ms]
(pass) proxy can handle redirects with non-TLS server > with chunked body #12007 [650.88ms]
(pass) proxy can handle redirects with TLS server > with chunked body #12007 [649.97ms]
(pass) non-TLS origin redirect through HTTPS proxy forwards every hop through the proxy [8.02ms]
(pass) unsupp
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/http/proxy.test.ts test/js/web/fetch/fetch-redirect.test.ts
bun test v1.4.0 (c50a91c20)

test/js/bun/http/proxy.test.ts:
(pass) GET non-TLS proxy -> non-TLS body type undefined [759.01ms]
(pass) POST non-TLS proxy -> non-TLS body type string [742.07ms]
(pass) GET TLS proxy -> non-TLS body type undefined [934.80ms]
(pass) GET non-TLS proxy -> TLS body type undefined [995.92ms]
(pass) POST non-TLS proxy -> TLS body type string [1016.42ms]
(pass) POST TLS proxy -> non-TLS body type string [401.94ms]
(pass) GET TLS proxy -> TLS body type undefined [650.76ms]
(pass) POST TLS proxy -> TLS body type string [572.68ms]
(pass) proxy can handle redirects with non-TLS server > with empty body #12007 [817.57ms]
(pass) proxy can handle redirects with non-TLS server > with body #12007 [923.98ms]
(pass) proxy can handle redirects with TLS server > with empty body #12007 [1008.17ms]
(pass) proxy can handle redirects with TLS server > with body #12007 [949.15ms]
(pass) proxy can handle redirects with non-TLS server > with chunked body #12007
... (truncated)

release with fix: 1 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 695ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/5] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m   Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m   Compiling�[0m bun_output v0.0.0 (/workspace/bun/src/output)
�[1m�[92m   Compiling�[0m bun_clap v0.0.0 (/workspace/bun/src/clap)
�[1m�[92m   Compiling�[0m bun_valkey v0
... (truncated)
diff hotspot
src/http/HTTPThread.rs                   |  7 +--
 src/http/lib.rs                          | 20 ++++++--
 src/url/lib.rs                           | 12 +++--
 test/js/bun/http/proxy.test.ts           | 87 ++++++++++++++++++++++++++++++++
 test/js/web/fetch/fetch-redirect.test.ts | 46 +++++++++++++++++
 5 files changed, 159 insertions(+), 13 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                      reads  edits  tests
src/http/HTTPThread.rs                        1      2      0
src/http/lib.rs                               3      2      0
src/url/lib.rs                                3      3      0
test/js/bun/http/proxy.test.ts                1      1      0
test/js/web/fetch/fetch-redirect.test.ts      1      1      0

RFC 3986 section 3.1 defines the URL scheme as case-insensitive, and
both curl and undici's EnvHttpProxyAgent accept `HTTP://` in
`http_proxy`. Bun's `fetch(url, { proxy })` option accepts it because
the WHATWG URL parser lowercases the scheme, but the `http_proxy` /
`HTTPS_PROXY` environment variables are parsed by `bun_url::URL::parse`,
which borrows the scheme slice without normalizing it. The proxy
protocol check in `HTTPThread` and the `is_http`/`is_https` helpers
compared the borrowed bytes exactly, so `http_proxy=HTTP://host:port`
rejected every request (fetch and `bun install` alike) with
`UnsupportedProxyProtocol`.

Make `is_http`, `is_https`, `is_s3`, `is_file`, and
`has_http_like_protocol` compare ASCII case-insensitively, and route
the inline protocol checks in `HTTPThread` through
`has_http_like_protocol`. This also fixes `get_port_auto()` defaulting
an `HTTPS://proxy` URL to port 80 instead of 443.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7178b135-2b7f-422e-b8bc-6bdae131d9d7

📥 Commits

Reviewing files that changed from the base of the PR and between 47597ab and c50a91c.

📒 Files selected for processing (5)
  • src/http/HTTPThread.rs
  • src/http/lib.rs
  • src/url/lib.rs
  • test/js/bun/http/proxy.test.ts
  • test/js/web/fetch/fetch-redirect.test.ts

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

@robobun

robobun commented Jul 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Reproduced: http_proxy=HTTP://127.0.0.1:<port> + fetch() fails with UnsupportedProxyProtocol; Location: HTTPS://host/... fails with UnsupportedRedirectProtocol. Both work via the explicit { proxy } option / curl / undici.

PR: #35144

CI (build 77893): the new tests in test/js/bun/http/proxy.test.ts and test/js/web/fetch/fetch-redirect.test.ts pass on every lane that ran. Remaining failures are unrelated to this diff:

  • test/js/node/test/parallel/test-net-connect-memleak.js and test-gc-http-client-connaborted.js on ubuntu 25.04 x64 / debian 13 x64: FinalizationRegistry/GC-timing assertions on net/node:http sockets; this diff only widens scheme string comparison and does not touch socket lifetime or GC. The memleak one also failed on build 77870 (before the second commit here).
  • darwin 26 aarch64 (2 jobs) failed outside the test runner with "The number of VMs exceeds the system limit" — the tart guest never booted.
  • Remaining entries are flaky retries (GitHub tarball 504s in the install suite, bake dev-server timeout on Windows, etc.).

Diff is ready for review.

@robobun

robobun commented Jul 22, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:32 AM PT - Jul 22nd, 2026

❌ @robobun, your commit c50a91c has 3 failures in Build #77893 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 35144

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

bun-35144 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Missing URL normalization for HTTP_PROXY and HTTPS_PROXY #16182 - Directly reports missing URL normalization for proxy env vars; the case-insensitive scheme comparison in this PR fixes the rejection of uppercase schemes like HTTP://proxy:8080

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

Fixes #16182

🤖 Generated with Claude Code

@robobun

robobun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Re #16182: that issue asks for full WHATWG normalization of the http_proxy/https_proxy URL before handing it to the HTTP thread. This PR is narrower: it makes the scheme comparison case-insensitive (RFC 3986 3.1), which removes the UnsupportedProxyProtocol rejection for HTTP:///Https:// and fixes the default-port pick, but it does not run the whole value through the WHATWG parser. Linked as related rather than "fixes".

Comment thread src/url/lib.rs
The redirect follower slices the scheme out of the raw Location header
bytes and checks it against "http"/"https" before WHATWG normalization
runs, so `Location: HTTPS://host/...` was rejected with
UnsupportedRedirectProtocol. Route those two comparisons through
eql_case_insensitive_ascii to match the proxy-protocol fix.
@robobun robobun changed the title http: compare proxy URL scheme case-insensitively http: compare URL scheme case-insensitively for proxy and redirect Location Jul 22, 2026

@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 follow-up commit addressed the two sibling Location-scheme comparisons I flagged, and the added redirect test covers them.

What was reviewed:

  • Confirmed eql_case_insensitive_ascii(.., true) length-checks first, so an empty protocol slice returns false without tripping the !a.is_empty() debug_assert.
  • Re-grepped src/http/ for == b"http"/b"https" — no remaining byte-exact scheme comparisons.
  • Checked is_file()/is_s3() callers: the bun_url::URL ones in fetch.rs receive WHATWG-normalized (already-lowercase) schemes, so widening is a no-op there; the other hits are unrelated Path::is_file/Blob::is_s3 methods.
  • Tests are hermetic (local net.createServer, subprocess for env-var isolation), drain pipes concurrently, and include the negative case (socks5:// still rejected).
Extended reasoning...

Overview

Swaps byte-exact scheme comparisons for ASCII-case-insensitive ones at every site that sees an un-normalized URL scheme: bun_url::URL::{is_http,is_https,is_s3,is_file,has_http_like_protocol}, the two proxy-protocol checks in HTTPThread::connect, and the two Location: scheme checks in the redirect follower. Adds subprocess-isolated tests for http_proxy=HTTP://… (accepted) / socks5:// (still rejected) and an in-process redirect test for Location: HTTP:// / Http:// / hTtP://.

Security risks

None identified. The change strictly widens which case-spellings of http/https/file/s3 are recognised, per RFC 3986 §3.1. For is_file() this is directionally safer (a FILE:// guard now also matches). The proxy path already treated an empty protocol as http-like; the negative test confirms unrecognised schemes remain rejected rather than silently going direct.

Level of scrutiny

Moderate — touches the HTTP client/proxy connect path and redirect follower, but the diff is a mechanical comparison swap through an existing in-tree helper with no control-flow, ownership, or lifetime changes. The URL helpers are shared, so I checked callers: fetch-side callers go through WHATWG normalization first (scheme already lowercase), so behaviour there is unchanged; the borrowing-parser paths (env-var proxy, raw Location: bytes) are exactly what the tests exercise.

Other factors

My prior review flagged the two src/http/lib.rs sibling sites; c50a91c fixes both and adds a covering test. I re-verified via grep that no == b"http"-shaped comparisons remain in src/http/. The eql_case_insensitive_ascii(.., true) helper short-circuits on length mismatch before its non-empty debug_asserts, so empty protocol slices (e.g. Location: ://foo) behave as before. Tests follow harness conventions (pipe draining via Promise.all, bunEnv spread with proxy env keys undefined, try/finally cleanup, port: 0).

@Jarred-Sumner
Jarred-Sumner merged commit 2cc3658 into main Jul 22, 2026
50 of 53 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/33fe9693/proxy-scheme-case-insensitive branch July 22, 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