Skip to content

Make S3 requests honor NO_PROXY and HTTPS_PROXY like fetch - #32046

Closed
robobun wants to merge 5 commits into
mainfrom
farm/d78f5e63/s3-no-proxy
Closed

robobun wants to merge 5 commits into
mainfrom
farm/d78f5e63/s3-no-proxy

Conversation

@robobun

@robobun robobun commented Jun 10, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #32045

Problem

With HTTP_PROXY set and NO_PROXY=localhost,127.0.0.1, fetch connects directly to a localhost endpoint while S3Client sends the request to the proxy:

const server = Bun.serve({ port: 0, fetch: () => new Response("ok") });

// fetch honors NO_PROXY: connects directly
await fetch(server.url);

// S3Client does not: the PUT is sent to HTTP_PROXY in absolute-URI form
await Bun.S3Client.file("key", {
  accessKeyId: "test", secretAccessKey: "test",
  region: "eu-west-3", bucket: "my_bucket",
  endpoint: server.url.href,
}).write("content");

Cause

Every S3 call site resolved the proxy with EnvLoader::get_http_proxy(true, None, None). With hostname: None the NO_PROXY filter is skipped entirely, and is_http: true reads HTTP_PROXY even when the S3 endpoint is https (so HTTPS_PROXY was never consulted for https endpoints either). The effective request host is only known after signing (endpoint option, virtual hosted style, bucket, region), so the call sites could not pass it.

Fix

Resolve the proxy where the signed request URL is known: the three AsyncHTTP setup sites (execute_simple_s3_request, list_objects, download_stream) now call a shared resolve_proxy_url(url, explicit) which matches fetch's behavior:

  • explicit proxy (fetch("s3://…", { proxy })) is used as-is, but still subject to NO_PROXY, same as FetchTasklet does for fetch's explicit proxy
  • otherwise HTTP_PROXY/HTTPS_PROXY is selected from the URL scheme with the NO_PROXY filter applied (EnvLoader::get_http_proxy_for)

The hostname-less get_http_proxy(true, None, None) lookups at the ~15 call sites (Blob, S3File, Store, ReadableStream, RequestContext) are deleted along with the proxy threading through the S3 client function signatures. The explicit-override channel stays for the fetch s3 streaming-upload path.

Behavior notes:

  • S3 requests to https endpoints now use HTTPS_PROXY instead of HTTP_PROXY, matching fetch and the usual convention. Previously HTTPS_PROXY was ignored for S3 and HTTP_PROXY applied to https endpoints.
  • fetch("s3://…", { body: stream }) without an explicit proxy previously ignored proxy env vars entirely; it now resolves them like every other S3 request (the non-streaming s3 fetch path already did via FetchTasklet).

Tests

test/js/bun/s3/s3-proxy.test.ts runs each operation in a child process (proxy env vars are read from the process env) against a local mock endpoint and a local mock proxy that record hits:

  • write / stream / list / writer bypass HTTP_PROXY when the endpoint host is in NO_PROXY (covers all three request-construction sites and the multipart path)
  • write goes through HTTP_PROXY when NO_PROXY does not match (proxy support still works)
  • explicit fetch proxy with a streaming s3 upload: used when NO_PROXY does not match, bypassed when it does
  • https endpoint with only HTTP_PROXY set connects directly

On the unfixed build: 6 fail, 2 pass (the two pass-through sanity tests). With the fix: 8 pass. The previously failing tests fail with output like:

expect(received).toEqual(expected)
  "proxyHits": [
+   "PUT http://localhost:38049/mybucket/key",
  ],
Rebase notes (conflicts resolved against current main)

Main's S3 rework landed between revisions (XML response parsing, shared-reference task setup, VM teardown tickets, narrowed field visibility), so the rebase onto current main had conflicts in 5 files. Resolution:

  • simple_request.rs: main now sets task fields in the struct literal and takes only a shared reference to the task afterwards, so resolve_proxy_url is called on the signed URL before task creation and the result passed as the proxy_url field, instead of assigning through &mut after.
  • client.rs / multipart.rs: re-applied the parameter removals and the empty-means-env proxy_url() accessor on top of main's pub(crate) visibility and JsResult signatures.
  • RequestContext.rs: kept main's new HEAD framing logic and the server/global_this bindings (now consumed by the Locked-body arm on main); only the proxy argument to the S3 stat call is removed.
  • Blob.rs: dropped the proxy lookup; main had already hoisted the poll_ref.ref_ my earlier revision touched.

Verified after rebase: the 8 proxy tests pass on the debug build. As an end-to-end check in a container that itself sets HTTP_PROXY + NO_PROXY=localhost,...: on main 55 tests in test/js/bun/s3/ fail (mock-server requests are sent to the proxy), with this change 0 fail.


[review] gate passed · iteration 8 · 9 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/s3/s3-proxy.test.ts"
bun test v1.4.1 (65362b53b)

test/js/bun/s3/s3-proxy.test.ts:
112 |       const { stdout, stderr, exitCode } = await runChild(endpoint.url.href, op, {
113 |         HTTP_PROXY: proxy.url.href,
114 |         NO_PROXY: "localhost,127.0.0.1",
115 |       });
116 | 
117 |       expect({ stdout, exitCode, stderr, proxyHits }).toEqual({
                                                            ^
error: expect(received).toEqual(expected)

  {
    "exitCode": 0,
-   "proxyHits": [],
+   "proxyHits": [
+     "PUT http://localhost:45349/mybucket/key",
+   ],
    "stderr": "",
    "stdout": 
  "ok:write
  "
  ,
  }

- Expected  - 1
+ Received  + 3

      at <anonymous> (/workspace/bun/test/js/bun/s3/s3-proxy.test.ts:117:55)
(fail) s3 proxy env vars > write bypasses HTTP_PROXY when the endpoint host is in NO_PROXY [400.31ms]
112 |       const { stdout, stderr, exitCode } = await runChild(endpoint.url.href, op, {
113 |         HTTP_PROXY: proxy.url.href,
114 |         NO_PROXY: "localhost,127.0.0.1",
115 |       
... (truncated)

release without fix: 6 FAILED
bun test v1.4.1-canary.1 (65362b53b)

test/js/bun/s3/s3-proxy.test.ts:
112 |       const { stdout, stderr, exitCode } = await runChild(endpoint.url.href, op, {
113 |         HTTP_PROXY: proxy.url.href,
114 |         NO_PROXY: "localhost,127.0.0.1",
115 |       });
116 | 
117 |       expect({ stdout, exitCode, stderr, proxyHits }).toEqual({
                                                            ^
error: expect(received).toEqual(expected)

  {
    "exitCode": 0,
-   "proxyHits": [],
+   "proxyHits": [
+     "PUT http://localhost:37801/mybucket/key",
+   ],
    "stderr": "",
    "stdout": 
  "ok:write
  "
  ,
  }

- Expected  - 1
+ Received  + 3

      at <anonymous> (/workspace/bun/test/js/bun/s3/s3-proxy.test.ts:117:55)
(fail) s3 proxy env vars > write bypasses HTTP_PROXY when the endpoint host is in NO_PROXY [37.68ms]
112 |       const { stdout, stderr, exitCode } = await runChild(endpoint.url.href, op, {
113 |         HTTP_PROXY: proxy.url.href,
114 |         NO_PROXY: "localhost,127.0.0.1",
115 |       });
116 | 
117 |       expect({ stdout, exitCode, stderr, proxyHits }).toEqual({
                                                            ^
error: expect(re
... (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/s3/s3-proxy.test.ts"
bun test v1.4.1 (65362b53b)

test/js/bun/s3/s3-proxy.test.ts:
(pass) s3 proxy env vars > stream bypasses HTTP_PROXY when the endpoint host is in NO_PROXY [301.53ms]
(pass) s3 proxy env vars > write bypasses HTTP_PROXY when the endpoint host is in NO_PROXY [422.35ms]
(pass) s3 proxy env vars > writer bypasses HTTP_PROXY when the endpoint host is in NO_PROXY [331.93ms]
(pass) s3 proxy env vars > list bypasses HTTP_PROXY when the endpoint host is in NO_PROXY [492.72ms]
(pass) s3 proxy env vars > write goes through HTTP_PROXY when NO_PROXY does not match [454.80ms]
(pass) s3 proxy env vars > explicit fetch proxy is bypassed when the endpoint host is in NO_PROXY [306.27ms]
(pass) s3 proxy env vars > explicit fetch proxy is used when NO_PROXY does not match [396.02ms]
(pass) s3 proxy env vars > write to an https endpoint does not use HTTP_PROXY [484.59ms]

 8 pass
 0 fail
 14 expect() calls
Ran 8 tests across 1 file. [3.38s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     d72a8f2387
  features     baseline

23 deps, 131 codegen, 1172 objects in 931ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1244] gen bindgenv2
[2/1244] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[3/1244] fetch zlib
[zlib] up to date
[4/1244] fetch tinycc
[tinycc] up to date
[5/1243] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[6/1216] gen ProcessBindingConstants.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingConstants.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingConstants.cpp
[7/1216] gen .bind.ts → GeneratedBindings.cpp
[8/1216] gen ErrorCode+*.h
[9/1216] install /workspace/bun
bun install v1.4.1-canary.1 (65362b53b)

Checked 26 installs across 63 packages (no changes) [79.00ms]
[10/1216] gen JSBuffer.lut.h
Generating /workspace/bun/build/release/codegen/JSBuffer.lut.h from /workspace/bun/src/jsc/bindings/JSBuffer.cpp
[11/1216] subst deps/zlib/zlib.h
[12/1216] install /workspace/bun/packages
... (truncated)
diff hotspot
src/runtime/server/RequestContext.rs     |  10 --
 src/runtime/webcore/Blob.rs              |  68 +----------
 src/runtime/webcore/ReadableStream.rs    |  10 --
 src/runtime/webcore/S3File.rs            |  12 --
 src/runtime/webcore/blob/Store.rs        |  21 ----
 src/runtime/webcore/s3/client.rs         |  40 +-----
 src/runtime/webcore/s3/multipart.rs      |   7 +-
 src/runtime/webcore/s3/simple_request.rs |  27 +++-
 test/js/bun/s3/s3-proxy.test.ts          | 204 +++++++++++++++++++++++++++++++
 9 files changed, 241 insertions(+), 158 deletions(-)

gate history · 4 passed · 0 rejected · iteration 8

evidence per changed file
file                                      reads  edits  tests
src/runtime/server/RequestContext.rs          4      7     10
src/runtime/webcore/Blob.rs                   8     14     10
src/runtime/webcore/ReadableStream.rs         1      1     10
src/runtime/webcore/S3File.rs                 1      1     10
src/runtime/webcore/blob/Store.rs             2      4     10
src/runtime/webcore/s3/client.rs              3     12     10
src/runtime/webcore/s3/multipart.rs           4      6     10
src/runtime/webcore/s3/simple_request.rs      6      7     10
test/js/bun/s3/s3-proxy.test.ts               2      4     10

root cause · written by the author bot

S3 request paths called the environment proxy lookup with no hostname, so the NO_PROXY exclusion list was never consulted and requests to excluded hosts were still routed through HTTP_PROXY, with https endpoints also incorrectly reading HTTP_PROXY. The fix removes proxy resolution from the S3 call sites and instead resolves the proxy at the three points where the signed request URL is known, using a new resolve_proxy_url helper that applies NO_PROXY to explicit proxies and otherwise selects HTTP_PROXY or HTTPS_PROXY by scheme via the same env loader helpers fetch uses. This makes S3Client h…

@robobun

robobun commented Jun 10, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:40 PM PT - Aug 27th, 2026

✅ @robobun, your commit d72a8f238708c9ec32570f4151b1b669f8811aa8 passed in Build #107221! 🎉


🧪   To try this PR locally:

bunx bun-pr 32046

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

bun-32046 --bun

@coderabbitai

coderabbitai Bot commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Proxy resolution was moved from many S3 call sites into request-time logic: S3 client APIs dropped proxy parameters and a new resolve_proxy_url helper applies NO_PROXY/env selection when the signed URL is available. Call sites now omit proxy wiring and tests validate behavior.

Changes

S3 proxy resolution refactoring

Layer / File(s) Summary
Proxy resolution infrastructure
src/runtime/webcore/s3/simple_request.rs, src/runtime/webcore/s3/client.rs
Added resolve_proxy_url(url, explicit) and updated simple-request initialization to resolve proxy per-request using the signed URL and NO_PROXY rules.
S3 client signature refactoring
src/runtime/webcore/s3/client.rs
Removed explicit proxy_url/proxy parameters from S3 client entrypoints (stat, download, download_slice, delete, list_objects, upload, writable_stream, download_stream, readable_stream) and stopped forwarding proxy overrides in simple-request options.
Multipart upload proxy behavior
src/runtime/webcore/s3/multipart.rs
Documented empty proxy as "defer to env per-part resolution" and changed proxy_url() to return None when proxy storage is empty.
Blob read/write operations cleanup
src/runtime/webcore/Blob.rs
Removed http_proxy_href() and all proxy extraction in Blob S3 read/write paths; S3 calls now pass None for proxy.
S3File and ReadableStream cleanup
src/runtime/webcore/S3File.rs, src/runtime/webcore/ReadableStream.rs
Removed env-based proxy lookups in S3File stat tasks (exists, stat, size) and in ReadableStream S3 path; call sites now omit proxy arguments.
Store operations cleanup
src/runtime/webcore/blob/Store.rs
Removed bun_url::URL import and per-callsite proxy derivation from S3Ext::unlink and S3Ext::list_objects; calls rely on internal request-time resolution.
RequestContext HEAD response refactoring
src/runtime/server/RequestContext.rs
Refactored HEAD response server-detached check to use this.server.is_none() and removed proxy argument from S3 stat call in the HEAD content-length path.
ReadableStream call-site adjustment
src/runtime/webcore/ReadableStream.rs
Updated readable_stream call sites to drop the removed proxy parameter.
Proxy behavior test suite
test/js/bun/s3/s3-proxy.test.ts
Added tests validating NO_PROXY filtering, explicit proxy override behavior, and HTTP_PROXY vs HTTPS_PROXY selection across multiple S3 and fetch-stream operations.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR fully addresses issue #32045 by implementing proxy resolution at the point where the signed request URL is known, applying NO_PROXY filtering and scheme-specific proxy selection for S3 requests…
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing proxy handling in S3 requests. File modifications, new test suite, and refactoring are all focused on resolving the proxy behavior issue.
Title check ✅ Passed The title clearly and concisely describes the main change: S3 requests now honor NO_PROXY and HTTPS_PROXY in the same way as fetch.
Description check ✅ Passed The description explains the problem, root cause, fix, behavior changes, and verification results. It provides the required implementation and testing information, although it uses different headings …
Full details: Linked Issues check

Explanation

The PR fully addresses issue #32045 by implementing proxy resolution at the point where the signed request URL is known, applying NO_PROXY filtering and scheme-specific proxy selection for S3 requests.

Full details: Description check

Explanation

The description explains the problem, root cause, fix, behavior changes, and verification results. It provides the required implementation and testing information, although it uses different headings from the repository template.


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

@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/s3/s3-proxy.test.ts`:
- Around line 117-122: The assertion is masking real stderr when exitCode === 0
by using `stderr: exitCode === 0 ? "" : stderr`; update the expect(...) object
to use the actual stderr value instead of conditionally blanking it (e.g., use
`stderr: stderr` or just `stderr`) so unexpected stderr is surfaced; apply this
change to the repeated assertions that construct the expected object containing
`stdout, exitCode, stderr, proxyHits` (the expect(...).toEqual calls around the
occurrences you flagged).
🪄 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: db81fbb0-bf15-47a2-80f8-b13bbddfdbd7

📥 Commits

Reviewing files that changed from the base of the PR and between 09703da and 240b463.

📒 Files selected for processing (9)
  • src/runtime/server/RequestContext.rs
  • src/runtime/webcore/Blob.rs
  • src/runtime/webcore/ReadableStream.rs
  • src/runtime/webcore/S3File.rs
  • src/runtime/webcore/blob/Store.rs
  • src/runtime/webcore/s3/client.rs
  • src/runtime/webcore/s3/multipart.rs
  • src/runtime/webcore/s3/simple_request.rs
  • test/js/bun/s3/s3-proxy.test.ts
💤 Files with no reviewable changes (3)
  • src/runtime/webcore/S3File.rs
  • src/runtime/webcore/ReadableStream.rs
  • src/runtime/webcore/blob/Store.rs
👮 Files not reviewed due to content moderation or server errors (3)
  • src/runtime/webcore/s3/simple_request.rs
  • src/runtime/webcore/s3/client.rs
  • src/runtime/webcore/s3/multipart.rs

Comment thread test/js/bun/s3/s3-proxy.test.ts Outdated
@robobun
robobun force-pushed the farm/d78f5e63/s3-no-proxy branch from 5d12ba2 to fd77777 Compare June 28, 2026 02:51

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

No issues found, but this changes proxy-resolution behavior across the S3 request paths (HTTPS_PROXY selection, NO_PROXY now applied to explicit { proxy }, env proxies now consulted for streaming s3 fetch), so it's worth a human pass on the intended semantics.

Extended reasoning...

Overview

The PR centralizes S3 proxy resolution into resolve_proxy_url in simple_request.rs, called at the three AsyncHTTP setup sites (execute_simple_s3_request, list_objects, download_stream) once the signed request URL is known. The ~15 call sites that previously threaded get_http_proxy(true, None, None) through stat/download/upload/delete/writable_stream/etc. are deleted along with the corresponding parameters, and MultiPartUpload::proxy_url() now returns None for an empty override so per-part requests fall through to env resolution. RequestContext.rs drops the now-unused global_this binding in the HEAD path. A new test file exercises write/stream/list/writer/fetch-stream against local mock endpoint+proxy servers in child processes.

Security risks

Proxy selection determines where outbound S3 traffic is routed, so a regression could send credentials/data to the wrong host or bypass a mandated egress proxy. The change aligns S3 with the existing fetch/FetchTasklet semantics (get_http_proxy_for + is_no_proxy), which reduces surprise, but it does introduce three documented behavior changes: https S3 endpoints now consult HTTPS_PROXY instead of HTTP_PROXY; an explicit { proxy } is now subject to NO_PROXY; and streaming fetch("s3://…") without an explicit proxy now reads env proxies where it previously ignored them. None of these look exploitable, but they are user-visible networking changes.

Level of scrutiny

This is production runtime networking code (Rust) spanning eight source files plus a test. Most of the diff is mechanical parameter-removal, and the new helper is small and mirrors fetch's resolution, but the behavior deltas and the breadth of touched call sites put this beyond what I'd auto-approve as a trivial change.

Other factors

The bug-hunting pass found nothing; CodeRabbit's only nit (conditional stderr masking in tests) was addressed in bf863ff and the thread is resolved. Test coverage looks solid (8 cases covering each request-construction site, the multipart path, explicit-proxy override, and scheme selection). No CODEOWNERS match these paths.

@robobun

robobun commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator Author

Confirming the three behavior deltas are intentional, and all three bring S3 in line with how fetch already resolves proxies (same get_http_proxy_for + is_no_proxy path):

  1. https S3 endpoints now consult HTTPS_PROXY instead of HTTP_PROXY. The old code hardcoded is_http: true at every call site, so HTTPS_PROXY was never consulted for S3 and HTTP_PROXY was (incorrectly) applied to https endpoints, which are the default for real AWS.
  2. An explicit { proxy } on fetch("s3://...") is now subject to NO_PROXY. This matches FetchTasklet, which already applies NO_PROXY to fetch's explicit proxy option.
  3. Streaming fetch("s3://...") without an explicit proxy now reads env proxies. Previously it silently ignored them while the non-streaming s3 fetch path honored them, so this removes an inconsistency within the s3 fetch path itself.

Test coverage in test/js/bun/s3/s3-proxy.test.ts exercises each: scheme selection (https endpoint + only HTTP_PROXY set connects directly), explicit-proxy NO_PROXY (bypassed when matched, used when not), and the streaming fetch path. Happy to adjust any of these if the intended semantics differ.

@robobun

robobun commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator Author

CI status for whoever picks this up: the diff is green, the red is CI infrastructure.

Build #66075 failed with zero jobs exiting nonzero. Every build-rust step passed on all lanes (this is a Rust-only change), the x64-asan lane built and ran its scheduled shards green, and many lanes fully passed (linux x64/baseline/asan/musl, android, freebsd, windows x64-baseline/aarch64). The failures are all build-cpp steps that expired (no agent allocated), which cascaded their build-bun and downstream test steps into waiting_failed/expired. None of this touches the change.

I have spent my automatic retrigger on an earlier build, so a maintainer re-run of CI should clear this. Rebased on current main; happy to rebase again if it drifts.

robobun added a commit that referenced this pull request Jul 6, 2026
The spawned fixture aborted on the x64-asan lane during child teardown, and
piping its stderr hid the reason. Run the same assertions in-process, the way
s3-storage-class.test.ts and s3-requester-pays.test.ts already do, so a crash
surfaces its own trace. Neutralize HTTP_PROXY around the suite because the S3
client does not honor NO_PROXY (#32046); those vars are unset on CI.
robobun added a commit that referenced this pull request Jul 8, 2026
The spawned fixture aborted on the x64-asan lane during child teardown, and
piping its stderr hid the reason. Run the same assertions in-process, the way
s3-storage-class.test.ts and s3-requester-pays.test.ts already do, so a crash
surfaces its own trace. Neutralize HTTP_PROXY around the suite because the S3
client does not honor NO_PROXY (#32046); those vars are unset on CI.
@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

This came up again independently: on current main (54d6d16) every S3 call site still resolves the proxy with get_http_proxy(true, None, None) (Blob.rs, blob/Store.rs, S3File.rs, ReadableStream.rs, server/RequestContext.rs), and with HTTP_PROXY set plus NO_PROXY=localhost,127.0.0.1,::1 an S3Client pointed at a local Bun.serve endpoint still sends PUT http://localhost:<port>/b/k to the proxy while fetch() of the same URL connects directly. In an environment where the proxy cannot reach back to localhost this also makes most of test/js/bun/s3/s3-storage-class.test.ts fail, since it points the client at a local server.

This PR is the fix for it, so no second PR is being opened. Note that the branch has drifted: it is about 1000 commits behind main and a trial merge now conflicts in src/runtime/server/RequestContext.rs, src/runtime/webcore/Blob.rs, src/runtime/webcore/s3/multipart.rs and src/runtime/webcore/s3/simple_request.rs, so it needs a rebase before it can land.

Comment thread src/runtime/webcore/s3/client.rs Outdated
Comment thread src/runtime/webcore/s3/multipart.rs Outdated
Comment thread src/runtime/webcore/s3/simple_request.rs Outdated
Comment thread src/runtime/webcore/s3/simple_request.rs Outdated
Comment thread src/runtime/webcore/s3/simple_request.rs Outdated
Comment thread src/runtime/webcore/s3/simple_request.rs
Comment thread test/js/bun/s3/s3-proxy.test.ts

@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 — my earlier nit on the https test title was addressed in 52b8db5.

What was reviewed:

  • resolve_proxy_url uses the shared get_http_proxy_for/is_no_proxy env helpers; verified ProxySettings::from_explicit (http/lib.rs:706) already applies NO_PROXY to fetch's explicit proxy, so the "explicit proxy subject to NO_PROXY" delta genuinely matches fetch.
  • All three request-construction sites (execute_simple_s3_request, list_objects, download_stream) plus the multipart path are covered by tests; proxy_url() returning None on empty correctly threads through to per-part env resolution.
  • Owned Box<[u8]> return keeps the existing lifetime invariant (env href can be freed by a process.env write); no new lifetime extensions.
Extended reasoning...

Overview

Fixes #32045: S3 requests ignored NO_PROXY and always read HTTP_PROXY regardless of endpoint scheme, because ~15 call sites resolved the proxy with get_http_proxy(true, None, None) before the signed request URL (and thus the effective host/scheme) was known. The fix deletes those lookups and introduces resolve_proxy_url(url, explicit) in simple_request.rs, called at the three AsyncHTTP setup sites where the signed URL is available. It routes through the same EnvLoader::get_http_proxy_for / is_no_proxy helpers that fetch already uses. MultiPartUpload::proxy_url() now returns None when the stored override is empty so per-part requests fall through to env resolution. 9 files, +241/−158, mostly deletions.

Security risks

None identified. Honoring NO_PROXY makes requests connect directly to hosts the user has explicitly excluded — this narrows proxy usage per the user's own env, it does not expand it. No auth, TLS, or credential-handling paths are touched; reject_unauthorized and signing are unchanged.

Level of scrutiny

Moderate. This is networking routing but not memory-safety-critical or auth-adjacent. The new function is ~15 lines calling existing shared helpers; the rest is deleting a duplicated anti-pattern. Three behavior changes are documented (https→HTTPS_PROXY, explicit proxy subject to NO_PROXY, streaming fetch("s3://…") now reads env proxies), all of which converge S3 with existing fetch behavior — I verified the "matches FetchTasklet" claim against ProxySettings::from_explicit in src/http/lib.rs, which does capture and apply NO_PROXY to fetch's explicit { proxy }.

Other factors

  • All prior review feedback is resolved: coderabbit's stderr-masking (fixed in ed2f20e), comment-cop's paragraph-comment flags (shortened in a2a878f), and my test-title nit (renamed in 52b8db5).
  • 8 tests in s3-proxy.test.ts cover write/stream/list/writer (all four request paths), the pass-through case, explicit-proxy + NO_PROXY both ways, and https scheme selection; per the PR evidence 6 fail on the unfixed build and all 8 pass with the fix under debug ASAN.
  • The owned Box<[u8]> return preserves the pre-existing lifetime contract on S3HttpSimpleTask::proxy_url (env-derived href can be freed by a concurrent process.env write), and the resolved value is stored on the task before the detach_lifetime_ref extension, so no new lifetime hazards.
  • Verified no remaining get_http_proxy(true, None, None) sites in the S3 path.

@robobun
robobun force-pushed the farm/d78f5e63/s3-no-proxy branch from 52b8db5 to bcb977f Compare August 17, 2026 19:00

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

I reviewed this PR and the bug hunting system found no issues; my earlier nit about the https test title was addressed in bcb977f. Because this ships three intentional behavior deltas to S3 proxy routing (https endpoints now consult HTTPS_PROXY instead of HTTP_PROXY, explicit {proxy} is now subject to NO_PROXY, and streaming fetch("s3://…") now reads env proxies), a maintainer sign-off on those semantics would be worthwhile.

Checked: resolve_proxy_url uses the same get_http_proxy_for/is_no_proxy helpers as install/upgrade/WebSocket; the owned Box<[u8]> return avoids the env-href lifetime hazard the old call sites documented. Verified MultiPartUpload::proxy_url() returning None on empty correctly routes each part request through env resolution in execute_simple_s3_request. The three AsyncHTTP setup sites plus the multipart path are all covered by the new subprocess tests.

Extended reasoning...

Overview

The PR moves S3 proxy resolution from ~15 call sites (Blob.rs, S3File.rs, Store.rs, ReadableStream.rs, RequestContext.rs) — which all called get_http_proxy(true, None, None) before the effective request host was known — to the three AsyncHTTP construction points (execute_simple_s3_request, list_objects, download_stream) where the signed URL is available. A new resolve_proxy_url helper (simple_request.rs:553-569) applies the same get_http_proxy_for + is_no_proxy logic that fetch, install, upgrade, and WebSocket already use. MultiPartUpload::proxy_url() now returns None when empty so per-part requests fall through to env resolution. A 204-line test file exercises write/stream/list/writer × NO_PROXY, the positive proxy path, explicit-proxy ± NO_PROXY, and https scheme selection, all in child processes.

Security risks

Proxy resolution affects where credential-bearing S3 requests are sent, so incorrect routing could leak signed requests to an unintended proxy. The change reduces that risk relative to the old code (which sent NO_PROXY-listed hosts to the proxy anyway). resolve_proxy_url is called on the JS thread before scheduling, reads env via the process-singleton loader, and returns an owned copy — no cross-thread borrow of env storage. No new user input parsing.

Level of scrutiny

Medium. The mechanics are largely deletions plus an 18-line helper that composes existing primitives. What warrants a human look is the three intentional behavior deltas the author documented: (1) https S3 endpoints — the AWS default — now consult HTTPS_PROXY instead of HTTP_PROXY; (2) an explicit {proxy} on fetch("s3://…") is now filtered by NO_PROXY; (3) streaming s3 fetch without an explicit proxy now reads env proxies where it previously ignored them. All three are corrections that align S3 with fetch, but they change observable behavior for existing setups.

Other factors

All prior review threads are resolved (CodeRabbit's stderr-masking, comment-cop's comment-length flags, my test-title nit). The gate evidence shows 6/8 tests fail on the unfixed ASAN build and 8/8 pass with the fix. The PR was rebased onto current main after conflicts and re-verified. No CODEOWNERS file gates these paths that I could find.

@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 earlier nit (https-proxy test title) is addressed and no bugs were found on re-review.

What was reviewed:

  • resolve_proxy_url semantics vs fetch's ProxySettings::from_explicit/from_env + resolve() — both apply NO_PROXY to explicit proxies and pick HTTP_PROXY/HTTPS_PROXY by scheme via the same env loader helpers.
  • All three AsyncHTTP setup sites (execute_simple_s3_request, list_objects, download_stream) now resolve against the signed URL and store an owned copy on the task; the multipart proxy_url() accessor returns None for empty so per-part requests fall through to env resolution.
  • Removed proxy_url params traced through every caller (Blob/S3File/Store/ReadableStream/RequestContext/client) — no leftover env lookups or unused imports; upload_stream retains the explicit-override channel for the fetch("s3://…", { proxy }) path.
Extended reasoning...

Overview

The PR moves S3 proxy resolution from ~15 call sites (which passed get_http_proxy(true, None, None) — no hostname, hardcoded http) to the three points where the signed request URL is known. A new resolve_proxy_url(url, explicit) helper in simple_request.rs mirrors fetch's hop-0 resolution: an explicit proxy is checked against env.is_no_proxy(hostname, host); otherwise env.get_http_proxy_for(url) picks by scheme with NO_PROXY applied. The result is stored as an owned Box<[u8]> on the task (documented: process.env writes can free the env loader's borrowed href). Net -158 production lines (parameter/lookup removal) + a 204-line test file.

Security risks

Proxy resolution is mildly security-relevant, but this change tightens behavior: NO_PROXY exclusions that were previously ignored for S3 are now honored, and https endpoints stop reading HTTP_PROXY (which was the wrong variable). The helper reuses the same EnvLoader::is_no_proxy / get_http_proxy_for that fetch, WebSocket (Bun__isNoProxy), and the CLI already use, so no new parsing surface. No auth/crypto/permissions code touched.

Level of scrutiny

Medium. Most of the diff is mechanical parameter removal; the new logic is ~15 lines that delegate to existing shared helpers. I cross-checked resolve_proxy_url against ProxySettings::from_explicit/from_env + resolve() in src/http/lib.rs (what FetchTasklet::queue uses) and the semantics match for the initial request. S3 does not pass proxy_settings for redirect-hop re-evaluation, but that is pre-existing and unchanged by this PR.

Other factors

The PR carries three intentional behavior deltas (https→HTTPS_PROXY, explicit proxy subject to NO_PROXY, streaming s3 fetch now reads env proxies) that were called out and justified as aligning with fetch's established behavior. The 8-test suite covers all three request-construction sites, the multipart path, explicit-proxy NO_PROXY (both matched and unmatched), and https scheme selection; it fails 6/8 on the unfixed build and passes 8/8 with the fix. All prior review feedback (CodeRabbit stderr assertion, comment-cop length, my test-title nit) is resolved. The commits since my last review are the nit fix, comment shortening, and a CI retrigger — no substantive change.

…t URL

S3 call sites resolved the proxy with get_http_proxy(true, None, None),
which skips the NO_PROXY filter (no hostname to match against) and reads
HTTP_PROXY even for https endpoints. fetch resolves the proxy from the
request URL, so S3Client behaved differently from fetch in proxied
environments.

Resolve the proxy where the signed request URL is known instead: the
three AsyncHTTP setup sites (execute_simple_s3_request, list_objects,
download_stream) now call resolve_proxy_url(), which applies NO_PROXY
and selects HTTP_PROXY vs HTTPS_PROXY from the URL scheme, matching
fetch. The explicit proxy from fetch("s3://...", { proxy }) is still
honored, subject to NO_PROXY like fetch's own explicit proxy handling.
The per-call-site env lookups are deleted.

Fixes #32045
@robobun
robobun force-pushed the farm/d78f5e63/s3-no-proxy branch from a826fc3 to d72a8f2 Compare August 28, 2026 03:22

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

Code review found no issues

No high-confidence issues detected in this change.

@robobun

robobun commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #42692, which landed the same per-request proxy resolution for S3 (by scheme, with NO_PROXY). On a debug build of main (55c1106) the repro from #32045 reaches the local server, and 7 of the 8 tests in this PR pass. The eighth expects NO_PROXY to override an explicit proxy on fetch("s3://..."), and #42692 keeps that proxy as given.

Coverage note: on main, test/js/bun/http/proxy.test.ts checks S3 through file().text(), stat and fetch("s3://"). The list, stream() and writer() cases from this PR's test have no equivalent there.

@robobun robobun closed this Sep 16, 2026
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.

S3Client ignores NO_PROXY when HTTP_PROXY is set

1 participant