Skip to content

S3: read an endpoint with a scheme exactly as new URL() does - #40067

Open
robobun wants to merge 1 commit into
mainfrom
farm/aead0c3c/s3-endpoint-scheme-first
Open

robobun wants to merge 1 commit into
mainfrom
farm/aead0c3c/s3-endpoint-scheme-first

Conversation

@robobun

@robobun robobun commented Aug 22, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • After S3: read the endpoint the way new URL() reads it #39933, three endpoint spellings that new URL() accepts still go elsewhere. http:/127.0.0.1:9 and http:127.0.0.1:9 are read as the host http and fail with DNSResolveFailed after a resolver query. http://127.0.0.1:9 is rejected with ERR_INVALID_ARG_TYPE. new URL() reads all three as 127.0.0.1:9. Same on 1.4.0.
  • Cause: URL::parse_s3_endpoint (src/url/lib.rs) asks URL::parse first whether the endpoint has a scheme, and hands the string to WTF::URL only after that. URL::parse recognizes a scheme only as scheme://. With one slash or none it takes http: for the host, so https:// is prepended and WTF::URL reads http as the host. Leading whitespace makes it find no host at all.

Fix

  • Give the string to WTF::URL first. When it parses with a host, that is the endpoint. Otherwise it is host[:port][/prefix] with https as the default, and URL::parse decides whether there is a host, as before.
  • WTF::URL is the parser behind new URL(), so an endpoint with a scheme now means exactly what new URL(endpoint) says. A scheme-less endpoint (localhost:9000, s3.example.com, [::1]:9000) has no host to WTF::URL and takes the old path unchanged. An endpoint without a host (//127.0.0.1:9) is still rejected.
  • Verified: test/js/bun/s3/s3-list-encode-overflow.test.ts, four new cases fail on main and on 1.4.0. The scheme-less and no-host cases are pinned as well. test/js/bun/s3/: 163 pass, 2 pre-existing flakes (notes).

Background

  • S3Credentials.endpoint stores host[:port][/prefix]. sign_request signs the part before the first / as the host and connects to it, so what parse_s3_endpoint stores decides where signed requests go.
  • Bun has two URL parsers. WTF::URL (WebKit) is the WHATWG parser behind new URL(). bun_url::URL::parse slices an href that is already normalized, and it is the only one that accepts a scheme-less host:port.
  • The error for a host-less endpoint still says must be of type string. Received type string. The code ERR_INVALID_ARG_TYPE is pinned by two tests in s3.test.ts, so this change leaves it alone.
Notes

Probe on main (d95bc353ee) and on release 1.4.0, presigned URL without the query:

"http:/127.0.0.1:1"        new URL host = 127.0.0.1:1   S3 -> https://http/127.0.0.1:1/b/k       (1.4.0: https://http:/127.0.0.1:1/b/k)
"http:127.0.0.1:1"         new URL host = 127.0.0.1:1   S3 -> https://http:127.0.0.1:1/b/k
"http:///127.0.0.1:1"      new URL host = 127.0.0.1:1   S3 -> ERR_INVALID_ARG_TYPE
"  http://127.0.0.1:1  "   new URL host = 127.0.0.1:1   S3 -> ERR_INVALID_ARG_TYPE
"//127.0.0.1:1"            new URL throws               S3 -> ERR_INVALID_ARG_TYPE

With the change the first four give http://127.0.0.1:1/b/k, the last one still throws. localhost:9000, s3.example.com, bucket.test.r2.cloudflarestorage.com, [::1]:9000, 127.0.0.1:9000/prefix/ and s3://bucket produce the same presigned URL before and after.

Why "parses with a host" and not "parses": WTF::URL reads localhost:9000 as the scheme localhost with the opaque path 9000, and my-host:9000/x the same way. Both have an empty host, so they fall through to the https default as before.

Why https:// is still prepended only when URL::parse sees no scheme: an input such as http://:9/x fails in WTF::URL and has the scheme http to URL::parse. Prepending would turn it into https://http://:9/x, which WTF::URL reads as host http. Leaving the prepend out keeps the fallback at what URL::parse reads, as today.

S3_ENDPOINT and AWS_ENDPOINT go through the same function (src/dotenv/env_loader.rs) and are covered by the existing child-process test.

The two failures in the S3 suite run are not related: s3-list-objects.test.ts "Should fall back to NoSuchKey" hits its 5 s budget next to an 18 s test in the same describe.concurrent block under ASAN (passes alone), and s3.test.ts "uploads a fetch response body via the native ByteStream" lost a part's byte count to received += (await ...) in one of four runs (#37205 fixes that test).

Rebased over #40238 (bun_core::String owns its WTF ref) and #40374 (eq_ascii): S3Endpoint::from_whatwg calls the whatwg::URL getters directly and compares the scheme with eq_ascii, the same changes main made to the inline code this PR moves into the helper. No other conflict.


[review] gate passed · iteration 4 · 2 files touched

fails on main (without fix)
ASAN without fix: 4 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-list-encode-overflow.test.ts"
bun test v1.4.1 (861e9ae04)

test/js/bun/s3/s3-list-encode-overflow.test.ts:
(pass) S3Client.list() option encoding > should not panic when prefix is longer than 1024 bytes when encoded [8.53ms]
(pass) S3Client.list() option encoding > should not panic when delimiter is longer than 1024 bytes when encoded [2.27ms]
(pass) S3Client.list() option encoding > should not panic when continuationToken is longer than 1024 bytes when encoded [1.95ms]
(pass) S3Client.list() option encoding > should not panic when startAfter is longer than 1024 bytes when encoded [2.40ms]
(pass) S3 object keys containing '?' or '#' > includes the full object key in the presigned URL path [9.80ms]
(pass) S3Client region option > rejects the region us-east-1/other.example.com because it is not a valid host name component [6.05ms]
(pass) S3Client region option > rejects the region us-east-1?x because it is not a valid host name component [2.04ms]
(pass) S3Client region option > rejects the region us-east-1#x because it
... (truncated)

release without fix: 4 FAILED
bun test v1.4.1-canary.1 (861e9ae04)

test/js/bun/s3/s3-list-encode-overflow.test.ts:
(pass) S3Client.list() option encoding > should not panic when prefix is longer than 1024 bytes when encoded [0.16ms]
(pass) S3Client.list() option encoding > should not panic when delimiter is longer than 1024 bytes when encoded [0.03ms]
(pass) S3Client.list() option encoding > should not panic when continuationToken is longer than 1024 bytes when encoded [0.02ms]
(pass) S3Client.list() option encoding > should not panic when startAfter is longer than 1024 bytes when encoded [0.01ms]
(pass) S3 object keys containing '?' or '#' > includes the full object key in the presigned URL path [0.22ms]
(pass) S3Client region option > rejects the region us-east-1/other.example.com because it is not a valid host name component [0.09ms]
(pass) S3Client region option > rejects the region us-east-1?x because it is not a valid host name component [0.01ms]
(pass) S3Client region option > rejects the region us-east-1#x because it is not a valid host name component
(pass) S3Client region option > rejects the region us east 1 because it is not a valid host name component
(pass) S3Client region option 
... (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-list-encode-overflow.test.ts"
bun test v1.4.1 (861e9ae04)

test/js/bun/s3/s3-list-encode-overflow.test.ts:
(pass) S3Client.list() option encoding > should not panic when prefix is longer than 1024 bytes when encoded [8.31ms]
(pass) S3Client.list() option encoding > should not panic when delimiter is longer than 1024 bytes when encoded [2.28ms]
(pass) S3Client.list() option encoding > should not panic when continuationToken is longer than 1024 bytes when encoded [1.92ms]
(pass) S3Client.list() option encoding > should not panic when startAfter is longer than 1024 bytes when encoded [1.79ms]
(pass) S3 object keys containing '?' or '#' > includes the full object key in the presigned URL path [10.21ms]
(pass) S3Client region option > rejects the region us-east-1/other.example.com because it is not a valid host name component [6.40ms]
(pass) S3Client region option > rejects the region us-east-1?x because it is not a valid host name component [2.08ms]
(pass) S3Client region option > rejects the region us-east-1#x because i
... (truncated)

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     b3921981d9
  features     baseline

23 deps, 129 codegen, 1172 objects in 777ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1244] install /workspace/bun
bun install v1.4.1-canary.1 (861e9ae04)

Checked 26 installs across 63 packages (no changes) [15.00ms]
[2/1244] gen ErrorCode+*.h
[3/1244] gen bindgenv2
[4/1244] install /workspace/bun/packages/bun-error
bun install v1.4.1-canary.1 (861e9ae04)

Checked 1 install across 2 packages (no changes) [3.00ms]
[5/1244] fetch tinycc
[tinycc] up to date
[6/1243] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[7/1216] install /workspace/bun/src/node-fallbacks
bun install v1.4.1-canary.1 (861e9ae04)

Checked 111 installs across 104 packages (no changes) [20.00ms]
[8/1216] gen .bind.ts → GeneratedBindings.cpp
[9/1216] fetch zlib
[zlib] up to date
[10/1216] gen node-fallbacks/react-refresh.js
Bundled 1 module in 13ms

  react-refresh.js  4.81 KB  (entry point)

[11/1216] gen bake.{client,server,error}.js
-> bake.client.js, bake.serv
... (truncated)
diff hotspot
src/url/lib.rs                                 | 51 +++++++++++++++++---------
 test/js/bun/s3/s3-list-encode-overflow.test.ts | 22 +++++++++--
 2 files changed, 52 insertions(+), 21 deletions(-)

gate history · 5 passed · 0 rejected · iteration 4

evidence per changed file
file                                            reads  edits  tests
src/url/lib.rs                                      7     13      0
test/js/bun/s3/s3-list-encode-overflow.test.ts      3      5      0

@coderabbitai

coderabbitai Bot commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

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

Or wait 23 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: 75836426-3ab0-4ded-bafe-78bb066f6d4e

📥 Commits

Reviewing files that changed from the base of the PR and between 0823e50 and b392198.

📒 Files selected for processing (2)
  • src/url/lib.rs
  • test/js/bun/s3/s3-list-encode-overflow.test.ts

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

@robobun

robobun commented Aug 22, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 9:29 PM PT - Aug 24th, 2026

✅ @robobun, your commit b3921981d93c0d464aa1f8d60175d980dcf466be passed in Build #105311! 🎉


🧪   To try this PR locally:

bunx bun-pr 40067

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

bun-40067 --bun

@robobun

robobun commented Aug 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: the change is ready. On the latest run (build 103449, ea26bf1, rebased onto main with #40062 and #40070) the new S3 test passes on every lane that ran it, and every red item is unrelated to this diff: the :darwin: any aarch64 - test-bun step failed before running any test (buildkite-agent artifact download timed out after 120s), and the remaining failures (hot.test.ts, require-cache.test.ts, install migration and lockfile tests, napi.test.ts, test-cluster-shared-leak.js) passed on retry or alone. Earlier runs were red on bun-pm.test.ts / bun-prune.test.ts snapshots and the src/js lint, both main breaks fixed by #40062 and #40070. This branch touches only src/url/lib.rs and the S3 endpoint test.

@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 — reorders parse_s3_endpoint to consult WTF::URL first so an endpoint with a scheme means exactly what new URL() says.

What was reviewed:

  • Traced the new control flow against the old for scheme-less inputs (localhost:9000, [::1]:9000, 127.0.0.1:9000/prefix/), scheme-relative //host, and s3://bucket — all take the same path as before; only the four newly-tested spellings change.
  • S3Endpoint::from_whatwg is a straight extraction of the previous inline WTF::URL→endpoint block plus an empty-host guard; whatwg::Parsed still frees via Drop and the OwnedString pattern is unchanged.
  • Both callers (credentials_jsc.rs, env_loader.rs) only read host_with_path/is_http, so no downstream adjustment needed.
Extended reasoning...

Overview

The PR touches one function, URL::parse_s3_endpoint in src/url/lib.rs, and its test file. It extracts the WTF::URL → S3Endpoint conversion into S3Endpoint::from_whatwg (adding an empty-host guard) and reorders the parse: try the WHATWG parser on the raw input first, and only fall back to Bun's slice-based URL::parse + https:// prepend when WTF::URL yields no host. Four new test cases (http:/…, http:…, http:///…, whitespace-padded) exercise the fix; four scheme-less cases and one no-host rejection are pinned to prevent regression.

Security risks

None material. The function decides which host a user-supplied S3 endpoint string names. The change makes that interpretation converge on new URL() semantics rather than diverge from it, so the set of hosts an input can name shrinks toward the standard rather than expanding. No new untrusted input surface, no credential handling touched, and both call sites (S3Credentials option parsing and S3_ENDPOINT/AWS_ENDPOINT env reading) already routed through this function.

Level of scrutiny

Medium. It is a small refactor of a single pure function with no unsafe, no FFI signature changes, and no memory-ownership changes — the same whatwg::Parsed RAII handle and OwnedString wrappers are used in the same order as before, just moved into a helper. The PR description is unusually thorough: it names each edge case (localhost:9000 reads as scheme localhost, http://:9/x must not be re-prefixed) and why the guard for it is shaped as it is. I walked each of those cases through both the old and new code and they line up.

Other factors

The one behavioral difference beyond the four fixed spellings is that inputs WTF::URL parses with an empty host (e.g. a data://x-shaped endpoint) now fall back to URL::parse instead of producing a host-less S3Endpoint; that is strictly less broken and not a plausible real endpoint. The extracted helper deduplicates what would otherwise be two copies of the hostname/pathname stitching, satisfying the "second time a block appears, extract it" rule. Tests are hermetic (presign is local, no network) and assert exact strings.

@robobun
robobun force-pushed the farm/aead0c3c/s3-endpoint-scheme-first branch 2 times, most recently from 22bf34d to ea26bf1 Compare August 22, 2026 08:39

@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 — reorders parse_s3_endpoint to consult WTF::URL first so a schemed endpoint means exactly what new URL() says.

What was reviewed:

  • Traced the three fall-through paths: WTF::URL-with-host wins; scheme-less inputs (localhost:9000, [::1]:9000) still get https:// prepended because WTF::URL yields an empty host; //host still returns None via URL::parse's empty host_with_path.
  • Checked the extracted S3Endpoint::from_whatwg copies everything into owned memory before Parsed drops the C++ WTF::URL; the empty-host guard is new but only replaces a previously-unusable empty result with the as_written fallback.
  • Four new test cases fail on main and pass here; scheme-less and no-host cases are pinned alongside.
Extended reasoning...

Overview

The PR reorders URL::parse_s3_endpoint in src/url/lib.rs so that the raw endpoint string is handed to WTF::URL (the WHATWG parser behind new URL()) first. If it parses with a non-empty host, that result is used directly. Only when WTF::URL yields no host does the function fall back to the existing URL::parse + optional https:// prepend. The duplicated host/pathname/protocol extraction is factored into S3Endpoint::from_whatwg. Tests add four schemed spellings that new URL() accepts (http:/…, http:…, http:///…, whitespace-padded) plus pin the scheme-less and no-host behaviours.

Security risks

parse_s3_endpoint decides the host that signed S3 requests are sent to, so a misparse is security-relevant. The change moves parsing closer to the WHATWG reference: previously http:/127.0.0.1:9 was misread as host http, which would send a signed request (and credentials) to whatever http resolves to. Now it goes to 127.0.0.1:9 as new URL() says. The //host rejection and the scheme-less default-to-https path are preserved and pinned by tests. No loosening of validation.

Level of scrutiny

Medium. The function is ~20 lines and the refactor is a reordering plus an extracted helper; every branch in the PR description is explained with a concrete input and the before/after presigned URL. I traced each case (localhost:9000 → WTF::URL scheme=localhost, empty host → falls through; //127.0.0.1:1 → WTF::URL fails, URL::parse empty host → None; http://:9/x → WTF::URL fails, URL::parse sees a protocol → no double-prepend, uses as_written). Memory: whatwg::Parsed is the RAII wrapper with Drop, and from_whatwg builds owned Vec<u8>/Box<[u8]> before the borrow ends — same pattern as the code it replaces. The one behaviour delta beyond the headline fix is that a WTF::URL result with an empty host now falls back to as_written instead of producing an empty host_with_path; that is strictly an improvement.

Other factors

The mechgate evidence shows the four new cases fail on main (ASAN debug) and pass with the fix, on both debug and release. The full test/js/bun/s3/ suite was run. No prior human review comments to address. The PR description's Notes section pre-emptively answers the two non-obvious design questions (why gate on "has a host" rather than "parses", and why keep the protocol.is_empty() guard on the prepend).

@robobun
robobun force-pushed the farm/aead0c3c/s3-endpoint-scheme-first branch from ea26bf1 to 938edec Compare August 24, 2026 05:05

@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 — reorders parse_s3_endpoint to consult WTF::URL first so schemed endpoints match new URL() exactly, with the scheme-less fallback unchanged.

What was reviewed:

  • Traced the new WTF::URL-first path against each existing input class (localhost:9000, s3://bucket, bare host, //host, IPv6) — all fall through to the same branch as before; only the four newly-tested spellings change.
  • S3Endpoint::from_whatwg is a byte-for-byte lift of the previous inline extraction (same hostname()/pathname() calls, same trailing-slash strip), so no ownership or encoding change.
  • Checked that the empty-host guard on the WTF result is what keeps localhost:9000 (parsed as scheme localhost, no host) on the https:// default — the PR notes explain this and the new defaults %j to https cases pin it.
Extended reasoning...

Overview

The PR touches one function, URL::parse_s3_endpoint in src/url/lib.rs, and its test file. It reorders the two-parser flow: instead of asking the in-tree URL::parse slicer whether a scheme exists and only then handing to WTF::URL, it hands the input to WTF::URL first. If WTF::URL parses with a non-empty host, that result is authoritative (matching new URL()). Otherwise the old path — URL::parse, prepend https:// when scheme-less, fall back to the sliced host — runs unchanged. The extraction of host[:port][/prefix] and is_http from a whatwg::URL is factored into S3Endpoint::from_whatwg, which is the identical sequence of calls the removed inline code made.

Security risks

The endpoint decides where signed S3 requests (and their credentials) go, so misparsing is security-adjacent. This change tightens alignment with the WHATWG parser rather than loosening it: the four newly-accepted spellings (http:/host, http:host, http:///host, whitespace-padded) now go where new URL() says instead of to a host literally named http or being rejected. No new input is accepted that new URL() would reject, and the //host no-scheme case is still rejected (pinned by a new test). I don't see an injection or host-confusion vector introduced here.

Level of scrutiny

Low-to-medium. ~30 lines of Rust net, no unsafe, no new FFI, no allocation-lifetime changes — the helper returns owned String values via existing getters and the Parsed handle drops as before. The logic is a reorder plus an extract-method refactor. The PR description is unusually thorough, walks each edge case (why "parses with a host" not "parses", why the https:// prepend stays gated on URL::parse's protocol check), and shows fails-on-main / passes-on-PR evidence for the four new cases.

Other factors

Tests use presign() so nothing dials a port. The new it.each for scheme-less defaults and the explicit //127.0.0.1:1 rejection test pin the fallback path against future regressions from this reorder. The single caller in env_loader.rs is already covered by the existing S3_ENDPOINT child-process test. No prior review comments to address; CI status per robobun is green on the touched test across lanes.

parse_s3_endpoint asked URL::parse first whether the endpoint had a
scheme, and only then handed it to WTF::URL. URL::parse recognizes a
scheme only as "scheme://", so http:/host:9 and http:host:9 were read
as the host "http", and "  http://host:9  " was rejected as having no
host. new URL() accepts all three and names host:9.

Give the string to WTF::URL first. When it parses with a host, that is
the endpoint. Otherwise it is host[:port][/prefix] with https as the
default, and URL::parse decides whether there is a host, as before.
@robobun
robobun force-pushed the farm/aead0c3c/s3-endpoint-scheme-first branch from 938edec to b392198 Compare August 25, 2026 03:10

@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 — focused reorder of parse_s3_endpoint so WTF::URL (the new URL() parser) gets first say when the endpoint has a scheme.

What was reviewed

  • Traced the new WTF::URL-first path against scheme-less inputs (localhost:9000, [::1]:9000, s3.example.com) — all yield an empty host in WTF::URL and fall through to the unchanged https:// default, now pinned by tests.
  • Confirmed S3Endpoint::from_whatwg is a byte-for-byte extraction of the old inline conversion plus the empty-host guard; is_http and path-suffix trimming are unchanged.
  • Checked the fallback branch when URL::parse sees a protocol: WTF::URL was already tried at the top, so skipping the https:// prepend there matches the old behavior for inputs like http://:9/x.
  • //127.0.0.1:1 still returns None (host_with_path empty) — pinned by the new rejection test.
Extended reasoning...

Overview

This PR reorders URL::parse_s3_endpoint in src/url/lib.rs so the input is handed to WTF::URL (the WHATWG parser behind new URL()) first, and only falls back to Bun's slice-based URL::parse when WTF::URL yields no host. The WTF::URL → S3Endpoint conversion is extracted into S3Endpoint::from_whatwg, which is now called from both the direct-parse and the https://-prepend paths. Net diff is ~30 lines of Rust plus 22 lines of test additions in test/js/bun/s3/s3-list-encode-overflow.test.ts.

Security risks

None identified. The S3 endpoint is user-supplied configuration (via the endpoint option or S3_ENDPOINT/AWS_ENDPOINT env), not untrusted network input, so there is no SSRF surface change — the user already controls where requests go. The change makes parsing more consistent with new URL(), which is what a user reading the docs would expect. Inputs that were previously rejected (//host) remain rejected; inputs that were previously misparsed as host http now resolve to the intended host, which is strictly a correctness improvement.

Level of scrutiny

Medium. parse_s3_endpoint decides which host signed S3 requests connect to, so a mis-parse could send credentials to the wrong place. However the change is a small, mechanical reorder of an existing two-parser flow, the extracted helper is a faithful copy of the prior inline code (verified line-by-line), and the PR description walks through each edge case (localhost:9000 as opaque scheme, http://:9/x fallback, s3://bucket, IPv6) with before/after probes. The new tests pin both the fixed cases and the unchanged scheme-less/rejection cases, and the gate evidence shows the four new cases fail on main and pass with the fix.

Other factors

  • No prior human or bot review comments to address; CI status per robobun is green with only unrelated flakes.
  • The refactor deduplicates the WTF::URL result handling into one helper rather than two inline copies, satisfying the "same block twice → extract" rule.
  • The one behavioral subtlety — why the https:// prepend is still gated on URL::parse seeing no protocol rather than always applied — is explained in the PR notes and I verified it: prepending onto http://:9/x would make WTF::URL read http as the host, so keeping the gate preserves today's fallback.
  • No CODEOWNERS entry covers src/url/.

@robobun

robobun commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

This also fixes a silent fallback in the env path. S3_ENDPOINT=" http://127.0.0.1:9000 " (surrounding whitespace) makes parse_s3_endpoint return None, and get_s3_credentials in src/dotenv/env_loader.rs then treats the variable as unset. The client signs and sends the request to s3.us-east-1.amazonaws.com with the user's key id instead of the configured private endpoint. Reproduced on 1.4.3:

S3_ENDPOINT=" http://127.0.0.1:9000 " bun -e 'console.log(new URL(new Bun.S3Client({bucket:"b",accessKeyId:"a",secretAccessKey:"b"}).presign("k")).host)'
s3.us-east-1.amazonaws.com

With this change WTF::URL strips the whitespace and the env value is used. The option form (endpoint: " http://... ") is already covered by the new it.each case.

This branch has not been deployed

No deployments
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