Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/sdk/abTesting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe("proxyToFallback", () => {
expect(init.redirect).toBe("manual");
});

it("strips hop-by-hop headers + host before forwarding", async () => {
it("strips hop-by-hop headers, preserving the original host", async () => {
fetchSpy.mockResolvedValue(new Response(null, { status: 204 }));

const request = new Request(`https://${REAL_HOST}/foo`, {
Expand All @@ -82,7 +82,9 @@ describe("proxyToFallback", () => {

const [, init] = fetchSpy.mock.calls[0];
const fwd = init.headers as Headers;
expect(fwd.get("host")).toBeNull();
// Host stays on the site's hostname — shared fallback origins (e.g. the
// EKS ingress LB) route by Host; sending the LB's own host 404s.
expect(fwd.get("host")).toBe(REAL_HOST);
expect(fwd.get("connection")).toBeNull();
expect(fwd.get("keep-alive")).toBeNull();
expect(fwd.get("transfer-encoding")).toBeNull();
Expand Down
18 changes: 14 additions & 4 deletions src/sdk/abTesting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ function fnv1a(str: string): number {

/**
* Hop-by-hop headers per RFC 7230 §6.1 — must not be forwarded through
* proxies. Plus `host`, which we always rewrite to the target origin.
* proxies.
*/
const HOP_BY_HOP_HEADERS = new Set([
"connection",
Expand Down Expand Up @@ -276,11 +276,18 @@ export function tagBucket(
* Proxy a request to the fallback origin with full hostname rewriting.
*
* Rewrites:
* 1. URL hostname → fallback origin
* 1. URL hostname → fallback origin (the connection target)
* 2. Set-Cookie Domain → real hostname
* 3. Body text: fallback hostname → real hostname (for Fresh partial URLs)
* 4. Location header → real hostname
*
* **The original `Host` header is preserved.** Fallback origins are shared
* entry points (e.g. the EKS ingress load balancer) that route to the site
* by `Host` — sending `Host: <fallbackOrigin>` instead returns the origin's
* 404 page. This mirrors the original cf-dispatch gateway, which forwarded
* the incoming headers (Host included) untouched. `x-forwarded-host` is
* still set for upstreams that read it.
*
* **`redirect: "manual"` is critical.** The request body is forwarded as a
* stream (`duplex: "half"`) and is consumed by this first fetch. If the
* upstream returns a 301/302 and we let CF auto-follow, the runtime would
Expand All @@ -297,16 +304,19 @@ export async function proxyToFallback(
const target = new URL(url.toString());
target.hostname = fallbackOrigin;

// Strip hop-by-hop headers + Host before forwarding. Without this the
// Strip hop-by-hop headers before forwarding. Without this the
// upstream may close the connection (Connection: close) or get confused
// by Transfer-Encoding/Upgrade meant for the original CF↔client hop.
const headers = new Headers();
for (const [key, value] of request.headers.entries()) {
const lk = key.toLowerCase();
if (lk === "host") continue;
if (HOP_BY_HOP_HEADERS.has(lk)) continue;
headers.set(key, value);
}
// Keep the site's hostname as Host — the fallback origin routes by it
// (see docstring). Set explicitly rather than trusting the incoming
// value so a mangled upstream Host can't leak through.
headers.set("host", url.hostname);
headers.set("x-forwarded-host", url.hostname);

const init: RequestInit = {
Expand Down