Skip to content

fetch: cap redirects at 20 and reject redirect failures with a TypeError - #33276

Open
robobun wants to merge 3 commits into
mainfrom
farm/4d4f05de/fetch-redirect-limit
Open

fetch: cap redirects at 20 and reject redirect failures with a TypeError#33276
robobun wants to merge 3 commits into
mainfrom
farm/4d4f05de/fetch-redirect-limit

Conversation

@robobun

@robobun robobun commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Related to #20486 (partially: that issue also covers DNS, connect, and URL-parse errors, which this PR leaves alone).

fetch() was not enforcing the WHATWG fetch specification's redirect limit, and redirect failures rejected with a plain Error instead of a TypeError.

https://fetch.spec.whatwg.org/#http-redirect-fetch step 5: "If request's redirect count is 20, then return a network error." A network error rejects the fetch() promise with a TypeError (https://fetch.spec.whatwg.org/#fetch-method).

Reproduction

let count = 0;
const server = Bun.serve({
  port: 0,
  fetch(req) {
    count++;
    const { pathname } = new URL(req.url);
    if (pathname === "/loop") return Response.redirect("/loop", 302);
    const [, , n, max] = pathname.split("/"); // /chain/<n>/<max>
    if (Number(n) >= Number(max)) return new Response("done");
    return Response.redirect(`/chain/${Number(n) + 1}/${max}`, 302);
  },
});

await fetch(`${server.url}chain/0/25`);            // Bun 1.4.0: 200    Node/spec: TypeError
await fetch(`${server.url}loop`).catch(e => count); // Bun 1.4.0: 127   Node/spec: 21
await fetch(`${server.url}chain/0/1`, { redirect: "error" })
  .catch(e => e.constructor.name);                 // Bun 1.4.0: "Error"  Node/spec: "TypeError"

Bun 1.4.0 (and main):

  • a 25-hop redirect chain resolves with a 200 after 26 requests
  • a self-redirect loop makes 127 round trips before rejecting (6x the traffic Node makes before giving up)
  • the rejection is a plain Error, which breaks err instanceof TypeError feature detection
    • error: The response redirected too many times. ... code: "TooManyRedirects"
    • error: UnexpectedRedirect fetching "..." ... code: "UnexpectedRedirect"

Node 26.3.0 rejects both after 21 requests with a TypeError.

Cause

  • make_client in src/http/AsyncHTTP.rs hardcoded remaining_redirect_count: 127.
  • FetchTasklet::on_reject converts every HTTP client failure into a SystemError, which SystemError__toErrorInstance materializes with ErrorType::Error.

Fix

  • src/http/lib.rs: add DEFAULT_REDIRECT_COUNT = 20 + 1 (do_redirect decrements before checking for 0, so the stored budget is one larger than the number of redirects followed) and use it in make_client. 20 redirects are followed and the 21st redirect response rejects, matching Node exactly. The existing maxRedirects fetch option still overrides the default, up to 126.
  • src/jsc/bindings/bindings.cpp: factor SystemError__toErrorInstance into a helper parameterized on JSC::ErrorType and add SystemError__toTypeErrorInstance. Exposed to Rust as SystemError::to_type_error_instance and Body::ValueError::SystemTypeError.
  • FetchTasklet::on_reject: route the redirect failure codes (TooManyRedirects, UnexpectedRedirect, RedirectURLInvalid, InvalidRedirectURL, RedirectURLTooLong, UnsupportedRedirectProtocol) through the new TypeError path. Every one of these is a network error under HTTP-redirect fetch. Messages and the code, path, and errno properties are unchanged, so existing err.code === "UnexpectedRedirect" handling (and the tests that assert it) keeps working; only the error's constructor changes.

Out of scope, deliberately: non-redirect network errors (ConnectionRefused, ECONNRESET, DNS and TLS failures, ...) stay plain Errors. Those carry code values users widely branch on, and widening further is a separate, larger compatibility decision.

How did you verify your code works?

New tests in test/js/web/fetch/fetch-redirect.test.ts, next to the existing redirect coverage. On main, 6 of the 7 fail (the 7th proves exactly 20 redirects is still allowed); all pass with the fix:

(pass) fetch() redirect limit > follows exactly 20 redirects by default
(pass) fetch() redirect limit > rejects the 21st redirect with a TypeError
(pass) fetch() redirect limit > a self-redirect loop makes exactly 21 requests before rejecting
(pass) fetch() redirect limit > exceeding an explicit maxRedirects rejects with a TypeError
(pass) fetch() redirect limit > redirect: "error" rejects with a TypeError
(pass) fetch() redirect limit > a redirect to a non-HTTP(S) scheme rejects with a TypeError
(pass) fetch() redirect limit > a redirect to an unparseable URL rejects with a TypeError

Also ran test/js/web/fetch/fetch.test.ts (maxRedirects and redirect: blocks), test/js/web/fetch/client-fetch.test.ts, test/js/web/fetch/fetch-http2-client.test.ts, test/js/first_party/undici/undici.test.ts, test/js/bun/http/proxy.test.ts, and test/integration/bun-types/bun-types.test.ts against the debug build; no new failures.


[review] gate passed · iteration 1 · 10 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/web/fetch/fetch-redirect.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (b145491fe)

test/js/web/fetch/fetch-redirect.test.ts:
(pass) fetch() preserves body on redirect [54.46ms]
(pass) fetch() rejects following a redirect to a Location with a non-HTTP scheme (file:/etc/hosts) [30.97ms]
(pass) fetch() rejects following a redirect to a Location with a non-HTTP scheme (file:hosts) [17.59ms]
(pass) fetch() normalizes a redirect Location containing a raw tab character before re-requesting [390.88ms]
DEBUG: Malformed HTTP response:
HTTP/1.1 302 Found
Location: /a�b
Content-Length: 0
Connection: close


(pass) fetch() rejects a redirect response whose Location contains a raw vertical tab character [54.73ms]
DEBUG: Malformed HTTP response:
HTTP/1.1 302 Found
Location: /a�b
Content-Length: 0
Conne
... (truncated)

release without fix: 8 FAILED
bun test v1.4.0-canary.1 (1498d7b77)

test/js/web/fetch/fetch-redirect.test.ts:
(pass) fetch() preserves body on redirect [3.72ms]
51 | 
52 |     const outcome = await fetch(new URL("/start", server.url)).then(
53 |       () => ({ rejected: false as const }),
54 |       e => ({ rejected: true as const, code: e.code }),
55 |     );
56 |     expect(outcome).toEqual({ rejected: true, code: "UnsupportedRedirectProtocol" });
                         ^
error: expect(received).toEqual(expected)

  {
-   "code": "UnsupportedRedirectProtocol",
+   "code": "ENOTFOUND",
    "rejected": true,
  }

- Expected  - 1
+ Received  + 1

      at <anonymous> (/workspace/bun/test/js/web/fetch/fetch-redirect.test.ts:56:21)
(fail) fetch() rejects following a redirect to a Location with a non-HTTP scheme (file:/etc/hosts) [1.52ms]
51 | 
52 |     const outcome = await fetch(new URL("/start", server.url)).then(
53 |       () => ({ rejected: false as const }),
54 |       e => ({ rejected: true as const, code: e.code }),
55 |     );
56 |     expect(outcome).toEqual({ rejected: true, code: "UnsupportedRedirectProtocol" });
                         ^
error: expect(received).toEqual(expected)

  
... (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/web/fetch/fetch-redirect.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (b145491fe)

test/js/web/fetch/fetch-redirect.test.ts:
(pass) fetch() preserves body on redirect [54.16ms]
(pass) fetch() rejects following a redirect to a Location with a non-HTTP scheme (file:/etc/hosts) [38.48ms]
(pass) fetch() rejects following a redirect to a Location with a non-HTTP scheme (file:hosts) [16.80ms]
(pass) fetch() normalizes a redirect Location containing a raw tab character before re-requesting [437.71ms]
DEBUG: Malformed HTTP response:
HTTP/1.1 302 Found
Location: /a�b
Content-Length: 0
Connection: close


(pass) fetch() rejects a redirect response whose Location contains a raw vertical tab character [54.71ms]
DEBUG: Malformed HTTP response:
HTTP/1.1 302 Found
Location: /a�b
Content-Length: 0
Conne
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     b145491fe6
  features     (none)

22 deps, 105 codegen, 1167 objects in 863ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1230] install /workspace/bun
bun install v1.4.0-canary.1 (1498d7b77)

Checked 124 installs across 170 packages (no changes) [4.00ms]
[2/1230] install /workspace/bun/packages/bun-error
bun install v1.4.0-canary.1 (1498d7b77)

Checked 1 install across 2 packages (no changes) [5.00ms]
[3/1230] install /workspace/bun/src/node-fallbacks
bun install v1.4.0-canary.1 (1498d7b77)

Checked 129 installs across 147 packages (no changes) [4.00ms]
[4/1230] gen ErrorCode+*.h
[5/1230] gen bindgenv2
[6/1230] fetch zlib
[zlib] up to date
[7/1230] gen .bind.ts → GeneratedBindings.cpp
[8/1230] gen ProcessBindingCon
... (truncated)
diff hotspot
docs/runtime/networking/fetch.mdx         |  32 ++++++++++
 packages/bun-types/globals.d.ts           |  10 ++-
 src/http/AsyncHTTP.rs                     |  10 +--
 src/http/lib.rs                           |  14 +++-
 src/jsc/SystemError.rs                    |  14 ++++
 src/jsc/bindings/bindings.cpp             |  17 ++++-
 src/jsc/bindings/headers.h                |   1 +
 src/runtime/webcore/Body.rs               |  10 +++
 src/runtime/webcore/fetch/FetchTasklet.rs |  16 ++++-
 test/js/web/fetch/fetch-redirect.test.ts  | 103 +++++++++++++++++++++++++++++-
 10 files changed, 213 insertions(+), 14 deletions(-)

gate history · 1 passed · 0 rejected · iteration 1

evidence per changed file
file                                       reads  edits  tests
docs/runtime/networking/fetch.mdx              2      2      0
packages/bun-types/globals.d.ts                2      2      0
src/http/AsyncHTTP.rs                          3      4      0
src/http/lib.rs                                2      2      0
src/jsc/SystemError.rs                         2      3      0
src/jsc/bindings/bindings.cpp                  4      2      0
src/jsc/bindings/headers.h                     1      1      0
src/runtime/webcore/Body.rs                    2      5      0
src/runtime/webcore/fetch/FetchTasklet.rs      2      2      0
test/js/web/fetch/fetch-redirect.test.ts       1      2      0

@robobun
robobun requested a review from alii as a code owner July 2, 2026 19:49
@github-actions github-actions Bot added the claude label Jul 2, 2026
@robobun

robobun commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 12:21 AM PT - Jul 29th, 2026

@robobun, your commit 459b2e3 has 1 failures in Build #84650 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 33276

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

bun-33276 --bun

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Native fetch incompatibilities with NodeJS error format and codes #20486 - This issue requests that fetch network errors be raised as TypeError per the WHATWG Fetch spec, which is exactly what this PR does for redirect-related errors

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

Fixes #20486

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in: 10 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: eceaa52c-121a-4a3b-beb8-d5b0f1258ea0

📥 Commits

Reviewing files that changed from the base of the PR and between 3f6a4e7 and 459b2e3.

📒 Files selected for processing (2)
  • docs/runtime/networking/fetch.mdx
  • packages/bun-types/globals.d.ts

Walkthrough

Redirect handling now uses a shared default budget, maps redirect failures to TypeError-backed network errors, and updates the public docs and tests to match the 20-redirect limit and redirect-specific error codes.

Changes

Redirect limit and TypeError classification

Layer / File(s) Summary
Redirect count constant and HTTP client wiring
src/http/lib.rs, src/http/AsyncHTTP.rs
Adds DEFAULT_REDIRECT_COUNT, uses it during HTTP client initialization, and clarifies the decrement-and-check redirect budget comments.
SystemError to TypeError conversion bindings
src/jsc/bindings/bindings.cpp, src/jsc/bindings/headers.h, src/jsc/SystemError.rs
Adds shared SystemError conversion logic parameterized by error type, exposes a TypeError binding, and adds the Rust method that invokes it.
ValueError SystemTypeError variant
src/runtime/webcore/Body.rs
Adds SystemTypeError(SystemError) and handles it in reset, JS conversion, and duplication.
Fetch redirect error mapping
src/runtime/webcore/fetch/FetchTasklet.rs
Maps redirect-specific failure codes to SystemTypeError instead of SystemError.
Docs and tests for redirect limit behavior
packages/bun-types/globals.d.ts, test/js/web/fetch/fetch-redirect.test.ts, docs/runtime/networking/fetch.mdx
Updates maxRedirects documentation and adds redirect-limit tests for default limits, explicit limits, and redirect failure cases.

Related PRs: None identified.

Suggested labels: rust, javascript, http, fetch

Suggested reviewers: None identified.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: redirect limits are capped at 20 and redirect failures now reject with a TypeError.
Description check ✅ Passed The description matches the required template and includes both the change summary and verification details.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

@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 didn't find any bugs, but this changes user-facing defaults (redirect limit 127→20) and the error constructor for redirect failures, so it's worth a human sign-off on the compatibility trade-off.

Extended reasoning...

Overview

This PR aligns fetch() redirect handling with the WHATWG Fetch spec: it lowers the default redirect budget from ~127 to 20 and changes redirect-related failures (TooManyRedirects, UnexpectedRedirect, RedirectURLInvalid, etc.) to reject with a TypeError instead of a plain Error. It touches the HTTP client (src/http/lib.rs, src/http/AsyncHTTP.rs), adds a new C++ binding SystemError__toTypeErrorInstance in bindings.cpp/headers.h, adds a SystemTypeError variant to Body::ValueError, routes six redirect error codes through it in FetchTasklet::on_reject, updates the maxRedirects type docs, and adds seven tests.

Security risks

None identified. The change tightens a limit rather than loosening one, and the error-type change preserves the existing code/path/errno properties.

Level of scrutiny

Moderate. The implementation is clean and well-tested, but this is a user-visible behavioral change in two dimensions: (1) the default redirect cap drops from ~127 to 20, which will make previously-succeeding long redirect chains start rejecting, and (2) the rejection constructor changes from Error to TypeError. Both are spec-correct and match Node, and the PR deliberately scopes the error-type change narrowly (redirect failures only, not all network errors), but a maintainer should confirm the compat trade-off is acceptable — particularly whether the default-limit drop needs a changelog/release-note callout.

Other factors

The PR also introduces new FFI surface (a parameterized systemErrorToErrorInstance helper in bindings.cpp and a new ValueError enum variant with matching deinit/to_js/dupe arms). All arms look correctly wired and the new tests exercise the boundary at exactly 20/21 hops plus each redirect-error code, but the combination of a default change, an error-type change, and new cross-language plumbing is more than a mechanical fix.

@robobun

robobun commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Leaving #20486 open rather than adding Fixes #20486. That issue asks for TypeError on every fetch network error (it leads with getaddrinfo ENOTFOUND and ConnectionRefused), plus Node's cause chain and error-code fidelity for invalid URLs and unsupported protocols. This PR only covers the redirect-failure family and deliberately leaves ConnectionRefused, DNS, and TLS failures as plain Errors, since their code values are widely branched on and widening further is a separate compatibility decision. So this is a partial step toward #20486, not a fix for it. I have added it to the description as a related issue.


On CI (updated, build #68045 is now final): the diff is green, the red is agent provisioning.

Final job tally for fa5b906: 83 passed, 0 failed, 187 waiting_failed (never started) and 16 canceled. Not a single job failed on its own. The waiting_failed jobs were all downstream of build-cpp / build-rust agents that never booted:

Failed to create agent for  x64 - build-cpp
Error: [robobun] Image not found: linux-x64-2023-amazonlinux-with-docker-v37

The same error took out build-cpp on x64, aarch64, macOS, Windows, and FreeBSD, and every other build in the pipeline hits it too (68047 through 68053, all on unrelated branches), so it is not this diff.

What did run on this commit:

  • build-rust passed on darwin-aarch64, darwin-x64, linux-x64, linux-x64-baseline, linux-x64-asan, linux-aarch64, aarch64-android, x64-android, freebsd
  • all three musl targets built a complete bun: aarch64-musl, x64-musl, x64-musl-baseline
  • 60 test-bun shards ran the suite on those three targets and all 60 passed (20 alpine aarch64, 20 alpine x64, 20 alpine x64-baseline)
  • cargo clippy, Format, TypeScript types, Lint JavaScript, binary-size: all green

So the full test suite has actually run against this change on three targets with no failures. Not pushing a retrigger: a new build lands in the same degraded agent pool. Happy to rebase or retrigger once the image is back, just say so.

@robobun

robobun commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Both bot reviews came back with no findings, so picking up the one open question from the Claude review: whether the compatibility trade-off is acceptable, and whether it needs a release-note callout.

Default limit, 127 to 20. Anything that needed more than 20 redirects now rejects. The spec and Node both stop at 20 (a 20-hop chain still resolves with a 200; the 21st redirect response is the network error, in both Node 26.3.0 and this branch, after exactly 21 requests). maxRedirects remains the escape hatch and still accepts up to 126. Worth knowing that the package manager shares this budget: src/install/npm.rs and src/install/NetworkTask.rs build their clients with FetchRedirect::Follow and the same default, so bun install now follows at most 20 redirects per request too.

Error constructor, Error to TypeError. TypeError extends Error, so catch (e) and e instanceof Error are unaffected, and the diff leaves code, path, errno, and the message strings untouched, so e.code === "TooManyRedirects" and e.code === "UnexpectedRedirect" keep working (the existing redirect: "error" #2819 test in fetch.test.ts asserts exactly that and still passes). The only thing that can break is code asserting e.name === "Error" or e.constructor === Error on a redirect failure. Grepping the suite, those assertions exist only for decompression errors and ECONNRESET, neither of which this PR touches.

Happy to drop the three rarely-hit codes (RedirectURLInvalid, InvalidRedirectURL, RedirectURLTooLong) from the TypeError set and keep it to TooManyRedirects / UnexpectedRedirect / UnsupportedRedirectProtocol if a narrower blast radius is preferred, though all six are network errors under the same spec section.

A release note for the default-limit change seems worthwhile. Say the word and I will add one.

@robobun
robobun force-pushed the farm/4d4f05de/fetch-redirect-limit branch from fa5b906 to 6215162 Compare July 4, 2026 17:52
@robobun

robobun commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto a8d0692 (6215162). One conflict, purely positional: #33193 / #33072 moved collectAsyncStackFramesFromPromise and Bun__attachAsyncStackFromPromise out of bindings.cpp into src/jsc/bindings/AsyncStackTrace.cpp, and they sat directly above SystemError__toErrorInstance, which this PR renames into a JSC::ErrorType-parameterized helper. Took main's deletion, reapplied the rename. git diff origin/main -- src/jsc/bindings/bindings.cpp is now exactly the three hunks the PR intended, nothing else.

Worth flagging one interaction with #33072, which landed in the meantime: it added a has_http_like_protocol() check on the relative-Location redirect path, so UnsupportedRedirectProtocol is now reachable from Location: file:hosts and not only from an absolute ftp:// URL. Both sites are in the set this PR maps to TypeError, which is what the spec wants (a redirect to a non-HTTP(S) scheme is a network error). #33072's new tests in fetch-redirect.test.ts assert on e.code rather than the constructor, so they pass unchanged.

Post-rebase, on the debug build: fetch-redirect.test.ts is 15/15 green (9 from main, 6 of the 7 new ones fail without the src/ change), client-fetch.test.ts 33/33, undici.test.ts 10/10, and the redirect block of fetch.test.ts 23/23 with the two long-standing debug-build failures (should allow very long redirect URLS timing out, should allow to follow redirect if connection is closed) reproducing identically on a8d0692 without this diff applied.

@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 `@src/http/lib.rs`:
- Around line 252-259: The redirect limit change in DEFAULT_REDIRECT_COUNT and
the redirect handling in HTTPClient::remaining_redirect_count/do_redirect is
correct, but this is a user-visible behavior change from the previous much
larger default. Update the release notes or changelog to explicitly call out
that fetch() now follows the spec’s 20-redirect budget by default so callers
depending on the old generous redirect behavior can adjust.
🪄 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: 8fa24466-642a-434b-a4f2-9dc1a13d325d

📥 Commits

Reviewing files that changed from the base of the PR and between fa5b906 and 6215162.

📒 Files selected for processing (7)
  • packages/bun-types/globals.d.ts
  • src/http/AsyncHTTP.rs
  • src/http/lib.rs
  • src/jsc/SystemError.rs
  • src/jsc/bindings/bindings.cpp
  • src/jsc/bindings/headers.h
  • src/runtime/webcore/Body.rs
💤 Files with no reviewable changes (1)
  • src/runtime/webcore/Body.rs

Comment thread src/http/lib.rs 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.

I didn't find any bugs, but this changes user-facing defaults (127→20 redirects, affecting bun install too) and error constructors, and you've raised open questions about the compat trade-off, narrowing the error-code set, and adding a release note — a maintainer should weigh in on those before merge.

Extended reasoning...

Overview

This PR caps fetch()'s default redirect limit at 20 (from 127) to match the WHATWG spec and Node.js, and makes redirect-family failures reject with a TypeError instead of a plain Error. It touches the HTTP client (src/http/lib.rs, src/http/AsyncHTTP.rs), adds a new C++/Rust binding for SystemError→TypeError (bindings.cpp, headers.h, SystemError.rs), adds a ValueError::SystemTypeError variant in Body.rs, routes six redirect error codes through it in FetchTasklet.rs, updates the maxRedirects JSDoc, and adds seven tests.

Security risks

None identified. The change tightens a limit rather than loosening one, and no auth/crypto/permission code is touched.

Level of scrutiny

This warrants maintainer review despite being mechanically clean, because it is a user-facing behavior change with non-local reach:

  • The 127→20 default affects every consumer of make_client, including — as the author notes — bun install (src/install/npm.rs, src/install/NetworkTask.rs). Registry redirect chains >20 hops are unlikely but not impossible with corporate proxies/mirrors.
  • The ErrorTypeError swap is spec-correct and mostly compatible (TypeError extends Error, code preserved), but it is still an observable API change.

Other factors

The author has left explicit open questions for a maintainer in the thread: whether the compat trade-off is acceptable, whether to narrow the TypeError set to three codes instead of six, and whether to add a release note. Those are product/API decisions that should be answered by a human, not a bot. The implementation itself looks correct and is well-tested (7 new tests, existing suites re-run), and the bug-hunting system found no issues.

@robobun

robobun commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator Author

Release-note ask from both reviews is handled in 3f6a4e7. Bun has no CHANGELOG file, so the behavior change is documented where a caller will hit it: docs/runtime/networking/fetch.mdx gained a Redirects section (the page did not mention redirects at all before), and the maxRedirects JSDoc now states the bounds, including that 0 rejects the first redirect rather than meaning unlimited. Every claim on that page was checked by running it. Thread resolved.

On the bun install reach that the Claude review flagged: npm itself caps registry redirects at 20. make-fetch-happen goes through minipass-fetch, whose request.js defaults follow to 20 (npm 11.16.0, node_modules/minipass-fetch/lib/request.js:120). So a registry or corporate mirror that needs more than 20 hops is already broken against npm, and matching that number is the conservative choice rather than a risky one.

On narrowing the TypeError set. I offered this earlier; withdrawing the offer, because the six codes are not a judgement call. Five of them map one-to-one onto an explicit "return a network error" clause:

code clause
RedirectURLInvalid HTTP-redirect fetch step 3, "If locationURL is failure"
InvalidRedirectURL step 3, reached through the relative-Location join instead of the absolute parse
UnsupportedRedirectProtocol step 4, "If locationURL's scheme is not an HTTP(S) scheme"
TooManyRedirects step 5, "If request's redirect count is 20"
UnexpectedRedirect HTTP fetch, redirect mode "error"

The sixth, RedirectURLTooLong, is Bun's own 128 KiB MAX_REDIRECT_URL_LENGTH rather than a spec clause, and it is only reachable if BUN_CONFIG_MAX_HTTP_HEADER_SIZE is raised well past its 16 KiB default. Classing it with the others keeps the contract simple: a redirect that cannot be followed always rejects with a TypeError, and callers never have to branch on the error class by Location length. Splitting it out buys nothing and costs a surprise.

So there is one call left for a maintainer, and it is the real one: accept the 127 → 20 default. maxRedirects is the escape hatch for anyone who needs more.

CI is clean on build #68324, no failures, now that the agent images are back.

@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 bugs found, but this changes a user-visible default (redirect cap 127→20, which the HTTP client shares with bun install) and swaps the rejection constructor to TypeError for six error codes — the compat/scope call you flagged is one a maintainer should make.

Extended reasoning...

Overview

This PR touches 10 files across the HTTP client (src/http/lib.rs, src/http/AsyncHTTP.rs), JSC bindings (src/jsc/bindings/bindings.cpp, headers.h, src/jsc/SystemError.rs), the fetch layer (src/runtime/webcore/Body.rs, FetchTasklet.rs), plus docs, type declarations, and tests. It (1) drops the default remaining_redirect_count from 127 to 21 (i.e. 20 followed redirects, matching the WHATWG spec and Node), and (2) routes six redirect-family failure codes through a new SystemError__toTypeErrorInstance binding so fetch() rejects with a TypeError instead of a plain Error. The bindings.cpp change is a clean refactor that parameterizes the existing helper on JSC::ErrorType; Body::ValueError gains a SystemTypeError variant handled in all three match sites (reset/to_js/dupe).

Security risks

None identified. The change tightens behavior (fewer redirects followed by default), which if anything reduces attack surface for redirect loops. No new untrusted-input parsing, no auth/crypto/permissions code touched.

Level of scrutiny

This warrants maintainer review, not because the implementation is questionable — it looks correct and is well-tested — but because it is a deliberate behavioral change on a hot, widely-used path:

  • The 127→20 default affects every fetch() caller and, as the author notes, the package manager (bun install uses the same make_client default). A registry chain longer than 20 hops would now fail.
  • Switching ErrorTypeError for six codes is spec-correct but observable to any caller checking err.name === "Error" or err.constructor === Error.
  • The author has an open question in the thread about whether to narrow the TypeError set to three codes instead of six; that's an API-surface decision a maintainer should answer.

Other factors

The implementation is clean: the C++ refactor preserves the existing SystemError__toErrorInstance symbol, the new Rust variant is handled everywhere the enum is matched, and the off-by-one budget math is documented and covered by tests that assert exact request counts (20 succeeds, 21 rejects, self-loop makes exactly 21 requests). Docs and .d.ts were updated. CI on the pre-rebase commit was green on the targets that ran; no bugs were surfaced by the multi-agent review. This is a good PR — it just isn't the kind of mechanical change that should merge without a human signing off on the compat trade-off.

@robobun
robobun force-pushed the farm/4d4f05de/fetch-redirect-limit branch from 3f6a4e7 to b145491 Compare July 9, 2026 00:45
@robobun

robobun commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto ab6eb2d (b145491). One conflict, and this time it was semantic rather than positional.

#31859 changed SystemError::to_error_instance (and deref, to_error_instance_with_async_stack, to_error_instance_with_info_object) to take self by value, because it releases the error's string refs and converting the same value twice would free strings the first Error still holds. ValueError::to_js now core::mem::takes the error out of its slot before calling it.

The new SystemError::to_type_error_instance this PR adds has exactly the same release-on-convert contract, so it got the same treatment: it takes self by value, and the ValueError::SystemTypeError arm in to_js also core::mem::takes before the call. The by-value signature is what makes the compiler catch a double-convert, so matching it here is not optional.

Post-rebase, on the debug build: fetch-redirect.test.ts is 15/15 green (6 of the 7 new tests fail without src/ applied), client-fetch.test.ts 33/33, undici.test.ts 10/10, fetch.test.ts -t maxRedirects 3/3, cargo clippy clean.

test/integration/bun-types/bun-types.test.ts fails 11/12 locally after the rebase, but that is npm, not this diff: typescript@latest flipped to 7.0.2 (the native rewrite, whose lib/ ships no lib.*.d.ts files at all), and the fixture in test/integration/bun-types/fixture/package.json depends on typescript: "latest". The failure reproduces identically on origin/main's packages/, so every branch will hit it until the fixture pins TypeScript.


CI for this head (build #70746, final): 281 passed, 3 failed, the diff is green. fetch-redirect.test.ts appears in no annotation, and neither does any other fetch test (fetch.test.ts, client-fetch.test.ts, fetch-http2-client.test.ts). The three failing jobs are pipeline-wide noise, each reproducing on at least five other unrelated branches in the last 40 builds:

lane test failure also seen on
windows-2019-x64 test/js/sql/postgres-binary-array-bounds.test.ts ERR_POSTGRES_CONNECTION_REFUSED (Postgres service down) #70735
alpine-3.23-x64-baseline test/regression/issue/26030.test.ts MySQL container application not healthy after 1m0s #70778, #70772, #70770, #70769, #70764
darwin-14-x64 test/js/bun/http/proxy-stress-concurrent.test.ts 1/1200 request failed under 1200-concurrent https-proxy load, on both mode=complete (no redirects) and mode=redirect #70781, #70767, #70764, #70760, #70758

The proxy-stress one superficially mentions "redirect", but mode=complete (no redirects at all) failed with the identical 1/1200 symptom, so it is the concurrency, not redirect handling.

Not pushing a retrigger: a fresh build hits the same unavailable Postgres and MySQL containers. This is ready for a maintainer.

Comment thread docs/runtime/networking/fetch.mdx Outdated
robobun added 2 commits July 29, 2026 01:20
https://fetch.spec.whatwg.org/#http-redirect-fetch step 5 returns a
network error once a request's redirect count reaches 20, and a network
error rejects the fetch() promise with a TypeError.

Bun's default budget was 127, so a 25-hop chain resolved with a 200 and
a redirect loop made 127 round trips before failing, and the rejection
was a plain Error.

Replace the hardcoded 127 with DEFAULT_REDIRECT_COUNT (= 21: 20
followed redirects, the 21st redirect response rejects). The
maxRedirects fetch option still overrides the default.

Add SystemError__toTypeErrorInstance and ValueError::SystemTypeError so
the redirect failure codes (TooManyRedirects, UnexpectedRedirect,
RedirectURLInvalid, InvalidRedirectURL, RedirectURLTooLong,
UnsupportedRedirectProtocol) reject with a TypeError that keeps its
code, path, and message properties.
fetch.mdx did not mention redirects at all, and maxRedirects was only
described in the .d.ts. Add a Redirects section covering the default
20-redirect budget, the TypeError rejection, and the bounds on
maxRedirects (0 rejects the first redirect, 126 is the maximum).
@robobun
robobun force-pushed the farm/4d4f05de/fetch-redirect-limit branch from b145491 to 1878855 Compare July 29, 2026 01:31
Comment thread src/http/AsyncHTTP.rs Outdated
Comment thread src/http/lib.rs Outdated
Comment thread src/http/lib.rs Outdated
Comment thread src/jsc/SystemError.rs Outdated
Comment thread src/jsc/bindings/bindings.cpp Outdated
Comment thread src/runtime/webcore/Body.rs Outdated
Comment thread src/runtime/webcore/fetch/FetchTasklet.rs Outdated
Comment thread src/runtime/webcore/fetch/FetchTasklet.rs Outdated
@robobun

robobun commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto 0316b6fa0c (1878855), across 532 commits. Two git conflicts and three semantic ones, all from refactors landing in exactly my hunks; the intended diff versus origin/main is unchanged.

Semantic updates to track main:

  • err!("TooManyRedirects") interned-name comparisons → http::Error::TooManyRedirects enum variants, via matches!(fail, http::Error::TooManyRedirects | http::Error::UnexpectedRedirect | ...). Replace bun_core::Error intern machinery with per-crate thiserror enums #33909 replaced the bun_core::Error intern machinery with per-crate thiserror enums, so the redirect-code check in FetchTasklet::on_reject was ported to the enum.
  • SystemError::dupe().clone(), and the new to_type_error_instance no longer calls a manual self.deref() after the FFI (the OwnedString fields now release on Drop). sys: make SystemError own its strings via OwnedString #35335 reworked SystemError to own its strings; to_type_error_instance now mirrors to_error_instance exactly: pub fn to_type_error_instance(self, global: &JSGlobalObject) -> JSValue { SystemError__toTypeErrorInstance(&self, global) }, same consume-by-value contract.
  • ValueError::dupe.clone() for the new SystemTypeError arm.

Git-level conflicts:

  • src/runtime/webcore/Body.rs: trivially the .dupe().clone() rename above.
  • test/js/web/fetch/fetch-redirect.test.ts: main appended a Location scheme is not lowercase block at the end of the file, I appended the redirect limit block at the same spot. Both kept, main's first.

Post-rebase, debug build: fetch-redirect.test.ts is 22/22 green (15 from main, my 7 new; 6 of the 7 fail without the src/ diff), client-fetch.test.ts 33/33, undici.test.ts 10/10, fetch.test.ts -t maxRedirects 3/3, cargo clippy clean, prettier clean.

Comment thread src/http/lib.rs Outdated
Comment thread src/http/lib.rs Outdated
Comment thread src/jsc/SystemError.rs Outdated
Comment thread src/jsc/bindings/bindings.cpp Outdated
Comment thread src/runtime/webcore/Body.rs Outdated
Comment thread src/runtime/webcore/fetch/FetchTasklet.rs Outdated
Comment thread src/runtime/webcore/fetch/FetchTasklet.rs Outdated
…mments

The fetch.mdx Redirects section claimed redirect: "manual" "works as
the specification describes" and then showed status 302 and a readable
Location header, which is the opposite of the spec's opaque-redirect
filtered response. Reword to state Bun's actual behavior explicitly.

Trim every new inline/doc comment to three lines or fewer per the
comment-length check.
@robobun
robobun force-pushed the farm/4d4f05de/fetch-redirect-limit branch from e98c2b9 to 459b2e3 Compare July 29, 2026 01:38
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.

1 participant