Which Cloudflare product(s) does this pertain to?
Wrangler
What versions are you using?
Reproduced identically on wrangler 4.86.0 (Node.js 20.20.2) and wrangler 4.107.0 β latest at time of filing (Node.js 22.23.1). npm 10.8.2, Windows 11 Pro (10.0.26200). The defect is in the ProxyWorker template JS, so it should be OS-independent.
Describe the Bug
Observed behavior
For an assets-only Worker with routes configured, wrangler dev rewrites the hostname inside Location headers produced by _redirects rules whose destination is an absolute URL on a different hostname that merely contains the route hostname as a substring β most commonly, any subdomain of the same zone. The result is a nonsense hostname.
With a route on example.com/* and the rule
/go https://books.example.com/read/ch01 302
local dev returns:
$ curl -sI http://127.0.0.1:8788/go
HTTP/1.1 302 Found
Location: https://books.127.0.0.1:8788/read/ch01
Status and path are correct; the hostname is mangled (books.example.com β books.127.0.0.1:8788). It is not even limited to subdomains β any hostname containing the route host as a substring is corrupted:
/tricky https://myexample.com/path 302 β Location: https://my127.0.0.1:8788/path
Expected behavior
Absolute redirect destinations pointing at other hostnames should pass through untouched β the local dev server does not serve books.example.com, so mapping that host to the local address can never be correct. The deployed Worker serves the correct Location: https://books.example.com/read/ch01. (Real-world case: our production assets-only Worker on duvera.app/* redirects /books/<slug> β https://books.duvera.app/read/β¦; correct in production, corrupted under wrangler dev, which breaks local verification of the redirect map.)
Reproduction
Three files, no server code:
wrangler.toml
name = "redirects-repro"
compatibility_date = "2026-04-26"
workers_dev = false
routes = [
{ pattern = "example.com/*", zone_name = "example.com" },
]
[assets]
directory = "./public"
public/_redirects
/go https://books.example.com/read/ch01 302
/apex https://example.com/somewhere 302
/external https://unrelated-domain.org/path 302
/tricky https://myexample.com/path 302
public/index.html β any content.
Run npx wrangler dev --port 8788, then curl -sI each path:
| rule destination |
Location returned by wrangler dev |
verdict |
https://books.example.com/read/ch01 |
https://books.127.0.0.1:8788/read/ch01 |
β subdomain of route zone mangled |
https://myexample.com/path |
https://my127.0.0.1:8788/path |
β substring-containing host mangled |
https://example.com/somewhere (= route host) |
https://127.0.0.1:8788/somewhere |
β οΈ host mapping presumably intended, but the scheme stays https while the dev server is plain HTTP, so following it fails |
https://unrelated-domain.org/path |
https://unrelated-domain.org/path |
β
untouched |
(Output identical on 4.86.0/Node 20 and 4.107.0/Node 22.)
Two extra data points that localize the defect to the dev proxy's innerβouter URL mapping (not the asset-worker redirects engine, whose output is correct):
- Removing
routes from wrangler.toml makes every Location pass through correctly.
- The substitution target tracks the incoming
Host header:
$ curl -sI http://127.0.0.1:8788/go -H "Host: something-else.dev"
Location: https://books.something-else.dev/read/ch01
Root cause
rewriteUrlRelatedHeaders() in packages/wrangler/templates/startDevWorker/ProxyWorker.ts (L348βL358), applied to every response header on the response path (L161 β rewriteUrlRelatedHeaders(res.headers, innerUrl, outerUrl)):
headers.forEach((value, key) => {
if (typeof value === "string" && value.includes(from.host)) {
headers.set(
key,
value.replaceAll(from.origin, to.origin).replaceAll(from.host, to.host)
);
}
});
value.includes(from.host) / value.replaceAll(from.host, to.host) is a boundary-less substring replace. With from.host = "example.com" (inferred from routes) and to.host = "127.0.0.1:8788":
books.example.com β books.127.0.0.1:8788 (subdomain matched as suffix)
myexample.com β my127.0.0.1:8788 (no left-boundary check at all)
It also explains row 3 of the table: .replaceAll(from.origin, to.origin) never matches these header values (in local dev from.origin is http://example.com while the header says https://example.com), so only the host-level replace fires and the https: scheme survives on a plain-HTTP dev address.
Suggested fix
Rewrite only when a URL actually parsed out of the header value has a host exactly equal to from.host, and rewrite scheme and host together β e.g. extract URL candidates with a regex, and for each candidate new URL(candidate), replace it with the rewritten URL only if url.host === from.host. That keeps the intended behavior (mapping the route/inferred host back to the local address, as needed for headers like Location pointing at the emulated host) while leaving books.example.com, myexample.com, etc. alone, and fixes the stale-scheme artifact as a side effect.
Related issues (checked β none cover this)
Which Cloudflare product(s) does this pertain to?
Wrangler
What versions are you using?
Reproduced identically on wrangler 4.86.0 (Node.js 20.20.2) and wrangler 4.107.0 β latest at time of filing (Node.js 22.23.1). npm 10.8.2, Windows 11 Pro (10.0.26200). The defect is in the ProxyWorker template JS, so it should be OS-independent.
Describe the Bug
Observed behavior
For an assets-only Worker with
routesconfigured,wrangler devrewrites the hostname insideLocationheaders produced by_redirectsrules whose destination is an absolute URL on a different hostname that merely contains the route hostname as a substring β most commonly, any subdomain of the same zone. The result is a nonsense hostname.With a route on
example.com/*and the rulelocal dev returns:
Status and path are correct; the hostname is mangled (
books.example.comβbooks.127.0.0.1:8788). It is not even limited to subdomains β any hostname containing the route host as a substring is corrupted:Expected behavior
Absolute redirect destinations pointing at other hostnames should pass through untouched β the local dev server does not serve
books.example.com, so mapping that host to the local address can never be correct. The deployed Worker serves the correctLocation: https://books.example.com/read/ch01. (Real-world case: our production assets-only Worker onduvera.app/*redirects/books/<slug>βhttps://books.duvera.app/read/β¦; correct in production, corrupted underwrangler dev, which breaks local verification of the redirect map.)Reproduction
Three files, no server code:
wrangler.tomlpublic/_redirectspublic/index.htmlβ any content.Run
npx wrangler dev --port 8788, thencurl -sIeach path:Locationreturned bywrangler devhttps://books.example.com/read/ch01https://books.127.0.0.1:8788/read/ch01https://myexample.com/pathhttps://my127.0.0.1:8788/pathhttps://example.com/somewhere(= route host)https://127.0.0.1:8788/somewherehttpswhile the dev server is plain HTTP, so following it failshttps://unrelated-domain.org/pathhttps://unrelated-domain.org/path(Output identical on 4.86.0/Node 20 and 4.107.0/Node 22.)
Two extra data points that localize the defect to the dev proxy's innerβouter URL mapping (not the asset-worker redirects engine, whose output is correct):
routesfromwrangler.tomlmakes everyLocationpass through correctly.Hostheader:Root cause
rewriteUrlRelatedHeaders()inpackages/wrangler/templates/startDevWorker/ProxyWorker.ts(L348βL358), applied to every response header on the response path (L161 βrewriteUrlRelatedHeaders(res.headers, innerUrl, outerUrl)):value.includes(from.host)/value.replaceAll(from.host, to.host)is a boundary-less substring replace. Withfrom.host = "example.com"(inferred fromroutes) andto.host = "127.0.0.1:8788":books.example.comβbooks.127.0.0.1:8788(subdomain matched as suffix)myexample.comβmy127.0.0.1:8788(no left-boundary check at all)It also explains row 3 of the table:
.replaceAll(from.origin, to.origin)never matches these header values (in local devfrom.originishttp://example.comwhile the header sayshttps://example.com), so only the host-level replace fires and thehttps:scheme survives on a plain-HTTP dev address.Suggested fix
Rewrite only when a URL actually parsed out of the header value has a host exactly equal to
from.host, and rewrite scheme and host together β e.g. extract URL candidates with a regex, and for each candidatenew URL(candidate), replace it with the rewritten URL only ifurl.host === from.host. That keeps the intended behavior (mapping the route/inferred host back to the local address, as needed for headers likeLocationpointing at the emulated host) while leavingbooks.example.com,myexample.com, etc. alone, and fixes the stale-scheme artifact as a side effect.Related issues (checked β none cover this)
localhostΒ #2433 (closed) β "OAuth redirect includeslocalhost"--host+ port mishandling), different symptom