Skip to content

fetch: follow same-origin redirects over the unix socket - #38394

Open
robobun wants to merge 1 commit into
mainfrom
farm/07cf4749/fetch-unix-redirect-same-origin
Open

robobun wants to merge 1 commit into
mainfrom
farm/07cf4749/fetch-unix-redirect-same-origin

Conversation

@robobun

@robobun robobun commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • fetch(url, { unix }): a redirect from the server on the unix socket is followed over TCP to the URL's host:port, even for a relative Location (Location: /v1/next). With the usual placeholder URL (http://localhost/...) the fetch fails with ConnectionRefused.
  • The hop has the same URL origin as the request, so the cross-origin strip never runs: Authorization meant for the unix server goes to whatever the URL's host:port names. curl and undici follow such redirects over the socket.
  • Cause: do_redirect and do_redirect_multiplexed (src/http/lib.rs) clear unix_socket_path before start() dials the next hop. Added in Support unix domain sockets in fetch() #9097 so a Location naming another server is followed over TCP, it also fired for same-origin hops.

Fix

  • A same-origin hop keeps unix_socket_path and is dialed over the same socket. A cross-origin hop still clears it, dials the new authority over TCP, and still loses Authorization/Cookie/Host through the existing strip. So a hop can only leave the socket without the credentials.
  • handle_response_metadata already computes is_same_origin for the header strip. It now also records it in a new state flag, redirect_is_cross_origin, which leave_unix_socket_for_cross_origin_redirect reads on both redirect paths, after the pool/close decision that reads the socket path.
  • The keep-alive local from fetch: pool the redirecting connection when the request went out in one write #37451 is folded back into the condition; it only existed because the path was cleared before that decision. A unix connection is still never pooled.
  • Verified: test/js/web/fetch/fetch.unix.test.ts, 6 of 15 tests fail without the src/http change, all pass with it. Also fetch-redirect, fetch-url-after-redirect, fetch-keepalive, and the cross-origin redirect tests in fetch.tls.test.ts and fetch.test.ts.

Background

  • unix makes fetch connect to a unix socket instead of resolving the URL's host. The URL only supplies the Host header and path, and is what a relative Location resolves against.
  • handle_response_metadata parses a 3xx, rewrites self.url and decides whether the hop is same-origin. do_redirect releases or closes the 3xx connection and calls start(), which connects for the new hop.
  • HttpThread::connect dials the unix socket whenever unix_socket_path is non-empty, otherwise the proxy or the URL's host:port.
Notes
  • New tests in the redirects over a unix socket block: relative Location, absolute Location on the same authority, a two-hop chain, a 307 resending its body, the http://localhost/ placeholder form, and the cross-origin exit without credentials. The existing pooling test now also checks that a plain fetch of the URL's host:port after the redirected unix fetch reaches the TCP server.
  • Fail-before was checked on bun 1.4.0 and on a debug build of main: the TCP listener receives the hop with authorization=Bearer daemon-token, and the placeholder form gets ConnectionRefused. A made-up hostname in the URL triggers a DNS lookup of that name.
  • Policy choice for reviewers: curl --unix-socket -L and undici's socketPath keep every hop on the socket, including cross-origin ones. This PR keeps the Support unix domain sockets in fetch() #9097 behavior for cross-origin hops, which the existing "handle redirect to non-unix" test asserts. Switching to curl semantics is one line (drop the clear in leave_unix_socket_for_cross_origin_redirect) plus flipping that test and the "leaves the socket" test.
  • The keep-alive local existed because the path used to be cleared before the pool/close decision, which is_keep_alive_possible() reads.
  • Rebase history: the first version of this branch renamed the then-existing clear_hostname_on_redirect flag and cleared the Host override in the same helper. fetch: never derive TLS SNI or certificate identity from the Host request header #35886 has since removed the Host override and that flag from main, so the flag here is new and the helper only touches the socket path. Earlier still, unix_socket_path was an owned ZigStringSlice shared bitwise with the JS-thread AsyncHTTP; main now stores a borrowed &'a [u8], so the clear is a plain assignment.
  • Found while working on this, handed off separately: with HTTP_PROXY/HTTPS_PROXY set in the environment, fetch(url, { unix }) writes a proxy-form request (or CONNECT) onto the unix socket.

no test proof · iteration 1 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/web/fetch/fetch.unix.test.ts

@coderabbitai

coderabbitai Bot commented Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c59e4764-24b8-44e0-914d-a75985022f92

📥 Commits

Reviewing files that changed from the base of the PR and between 97420a7 and 97305d7.

📒 Files selected for processing (3)
  • src/http/InternalState.rs
  • src/http/lib.rs
  • test/js/web/fetch/fetch.unix.test.ts

Included review availability: Your plan provides up to 5 included reviews per hour; 0 remain after this review.


Walkthrough

Redirect handling now preserves Host overrides and Unix-socket routing for same-origin redirects. Cross-origin redirects clear both states. HTTP/1.1 pooling rechecks keep-alive eligibility, and Unix-socket fetch tests cover the updated behavior.

Changes

Redirect routing

Layer / File(s) Summary
Redirect state contract
src/http/InternalState.rs, src/http/lib.rs
redirect_is_cross_origin replaces clear_hostname_on_redirect and defaults to false.
Origin-sensitive redirect cleanup
src/http/lib.rs
Shared redirect handling preserves same-origin Host and Unix-socket state, clears both for cross-origin redirects, and recalculates keep-alive eligibility.
Redirect behavior validation
test/js/web/fetch/fetch.unix.test.ts
Tests cover pooling, same-authority and cross-authority redirects, chained redirects, 307 body replay, socket routing, and authorization headers.

Suggested reviewers: dylan-conway, jarred-sumner, alii

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly and concisely describes the main change: same-origin fetch redirects continue over the Unix socket.
Description check ✅ Passed The description explains the problem, fix, implementation details, policy choice, and verification results. It does not use the exact template headings, but it provides the required information and is…
Full details: Description check

Explanation

The description explains the problem, fix, implementation details, policy choice, and verification results. It does not use the exact template headings, but it provides the required information and is complete enough for review.


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

@robobun

robobun commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:34 PM PT - Aug 26th, 2026

❌ @robobun, your commit c560a63 has 2 failures in Build #106399 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 38394

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

bun-38394 --bun

@robobun

robobun commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. Rebased onto main (c560a63).

CI on this head (build 106399) has two red tests, both also red on main's own build 106385 for the rebase base fb657755e9, on the same lanes, and both outside this PR (reported for main-break triage):

  • test/bundler/bun-build-api.test.ts: three stale BuildArtifact.hash snapshots, every lane.
  • test/js/web/url/url.test.ts: the Unicode 16 IDNA case throws Invalid URL on darwin x64.

Everything else is green or passed on retry, and the run before the rebase (build 106010) was 181 of 181 green.

Reproduced on bun 1.4.0 and on a debug build of main: a unix server answering 302 Location: /v1/next made the next hop arrive at the TCP listener named by the URL's host:port, Authorization included; with http://localhost/... as the URL the fetch failed with ConnectionRefused instead. With this branch both requests arrive at the unix server and the TCP listener sees nothing; a Location on another authority still goes out over TCP, without the credentials.

Proof is test/js/web/fetch/fetch.unix.test.ts: 6 of the 15 tests fail without the src/http change and all pass with it.

@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 didn't find any bugs. The refactor is clean and the ordering invariants (pool/close before the flag is consumed, state.reset() after) are preserved on both redirect paths. Because it changes redirect connection routing and credential-carrying behavior in the core HTTP client — and relies on the bitwise-copy ownership dance for unix_socket_path — a maintainer look would still be worthwhile.

What was reviewed

  • is_keep_alive_possible() folded back inline: still sees unix_socket_path (now forgotten only after the pool/close decision), so unix connections are still never pooled.
  • redirect_is_cross_origin is set unconditionally per hop and no longer cleared in the helper — verified state.reset() runs right after in both do_redirect and do_redirect_multiplexed, so no stale-flag carryover.
  • Cross-origin exit still forgets (not drops) the path; same-origin keeps it, which is safe since the HTTP-thread copy never owned it to begin with.
  • Test precondition server.url.origin !== origin guards the /leave case against the two origins accidentally coinciding.
Extended reasoning...

Overview

The PR fixes fetch(url, { unix }) so that a same-origin redirect is followed over the unix socket instead of dialing TCP to the URL's placeholder host:port. It touches src/http/lib.rs (both do_redirect and do_redirect_multiplexed), renames clear_hostname_on_redirect → redirect_is_cross_origin in src/http/InternalState.rs, extracts a shared leave_origin_for_cross_origin_redirect helper, and adds a comprehensive describe block plus strengthens the existing pooling test in test/js/web/fetch/fetch.unix.test.ts.

Security risks

This is itself a security fix: previously a same-origin redirect from a unix server would carry Authorization/Cookie to whatever TCP host:port the placeholder URL named (nothing was stripped because the origin matched). The new behavior keeps the hop on the socket for same-origin and, for cross-origin, still drops the socket path and still strips credentials via the existing cross-origin header strip — so a hop can only leave the socket without credentials. I checked that the is_same_origin computation and the credential-strip logic in handle_response_metadata are unchanged; the flag is now assigned unconditionally (= !is_same_origin) rather than only set-to-true, which is equivalent given state.reset() clears it between hops.

Level of scrutiny

High. This is the core HTTP client's redirect path, involves credential routing, and leans on a subtle memory-ownership pattern (the HTTP-thread HTTPClient is a ptr::read copy of the JS-thread original, so unix_socket_path must be ManuallyDrop-forgotten rather than dropped). The refactor moves that forget from unconditional-and-early to conditional-and-late; I traced that the pool/close decision in do_redirect still runs with the path present (so is_keep_alive_possible() still returns false for unix), and that do_redirect_multiplexed calls the helper before state.reset() discards the flag. The reasoning checks out, but this is exactly the kind of ordering-sensitive change a maintainer familiar with the HTTP-thread copy semantics should confirm.

Other factors

Test coverage is thorough (relative/absolute/chain/307-resend/placeholder/cross-origin-exit) with strong assertions on both what the unix and TCP servers actually received, including the authorization header value. The updated pooling test now also verifies a plain TCP fetch after the unix redirect reaches the TCP server. The PR description documents that the new tests fail on main and pass with the fix, and that adjacent redirect suites still pass. No prior human review on the PR yet.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Nothing to change from the review above. For the maintainer pass it asks for, the one policy choice in this PR is the cross-origin case:

  • Same-origin hop (relative Location, or an absolute one repeating the URL's authority): stays on the unix socket. This is the bug fix.
  • Cross-origin hop (Location naming another authority): unchanged from today, dialed over TCP with Authorization/Cookie/Host stripped by the existing cross-origin rule. That behavior dates from the original unix-socket support in Support unix domain sockets in fetch() #9097 and is what the existing "handle redirect to non-unix" test asserts, so I kept it.

curl (--unix-socket -L) and undici (socketPath) instead keep every hop on the socket. If that is preferred, the change is to drop the unix_socket_path line from leave_origin_for_cross_origin_redirect and flip the "handle redirect to non-unix" and "leaves the socket" tests; the rest of the PR stays as is.

@robobun
robobun force-pushed the farm/07cf4749/fetch-unix-redirect-same-origin branch from 715f8ef to 97305d7 Compare August 26, 2026 02:22
Comment thread src/http/InternalState.rs Outdated
Comment thread src/http/lib.rs Outdated
Comment thread src/http/lib.rs Outdated
Comment thread src/http/InternalState.rs Outdated
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.

Code review found no issues

No high-confidence issues detected in this change.

With fetch(url, { unix }), do_redirect forgot the socket path before
dialing the next hop, so every followed redirect (including a relative
Location from the unix server) was dialed over TCP to the URL's host:port,
and since that hop is same-origin the Authorization header went with it.

Keep the socket path across same-origin hops; a cross-origin hop still
leaves the socket and dials the Location's authority, as before.
@robobun
robobun force-pushed the farm/07cf4749/fetch-unix-redirect-same-origin branch from 954c4eb to c560a63 Compare August 26, 2026 22:07

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

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