Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 1bf7600

Browse files
mrbbothnrqervzaramel
committed
Unwrap opaqueredirect fetch responses, closes #133
Also sets the `redirect` mode of incoming requests to `manual`: https://developers.cloudflare.com/workers/runtime-apis/request#requestinit This ensures redirects are proxied to the end user, meaning cookies set in responses are stored in the browser. Co-Authored-By: Henrique Sarmento <[email protected]> Co-Authored-By: Vinicius Zaramella <[email protected]>
1 parent be3013a commit 1bf7600

File tree

4 files changed

+79
-4
lines changed

4 files changed

+79
-4
lines changed

packages/core/src/standards/http.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,9 @@ export class Response<
571571
return this[_kInner].statusText;
572572
}
573573
get type(): ResponseType {
574-
return this[_kInner].type;
574+
throw new Error(
575+
"Failed to get the 'type' property on 'Response': the property is not implemented."
576+
);
575577
}
576578
get url(): string {
577579
return this[_kInner].url;
@@ -647,7 +649,28 @@ export async function fetch(
647649
}
648650

649651
// Convert the response to our hybrid Response
650-
const res = new Response(baseRes.body, baseRes);
652+
let res: Response;
653+
if (baseRes.type === "opaqueredirect") {
654+
// Unpack opaque responses. This restriction isn't needed server-side,
655+
// and Cloudflare doesn't support Response types anyway.
656+
// @ts-expect-error symbol properties are not included type definitions
657+
const internalResponse = baseRes[fetchSymbols.kState].internalResponse;
658+
const headersList = internalResponse.headersList;
659+
assert(headersList.length % 2 === 0);
660+
const headers = new Headers();
661+
for (let i = 0; i < headersList.length; i += 2) {
662+
headers.append(headersList[i], headersList[i + 1]);
663+
}
664+
// Cloudflare returns a body here, but undici aborts the stream so
665+
// unfortunately it's unusable :(
666+
res = new Response(null, {
667+
status: internalResponse.status,
668+
statusText: internalResponse.statusText,
669+
headers,
670+
});
671+
} else {
672+
res = new Response(baseRes.body, baseRes);
673+
}
651674

652675
await waitForOpenInputGate();
653676
return withInputGating(res);

packages/core/test/standards/http.spec.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,6 @@ test("Response: constructing from BaseResponse doesn't create new BaseResponse u
608608
t.is(res.status, base.status);
609609
t.is(res.ok, base.ok);
610610
t.is(res.statusText, base.statusText);
611-
t.is(res.type, base.type);
612611
t.is(res.url, base.url);
613612
t.is(res.redirected, base.redirected);
614613

@@ -800,6 +799,14 @@ test("Response: can use byob reader when cloning", async (t) => {
800799
t.is(await byobReadFirstChunk(clone.body), "body");
801800
t.is(await byobReadFirstChunk(res.body), "body");
802801
});
802+
test("Response: type throws with unimplemented error", async (t) => {
803+
const res = new Response();
804+
t.throws(() => res.type, {
805+
instanceOf: Error,
806+
message:
807+
"Failed to get the 'type' property on 'Response': the property is not implemented.",
808+
});
809+
});
803810

804811
test("withWaitUntil: adds wait until to (Base)Response", async (t) => {
805812
const waitUntil = [1];
@@ -820,7 +827,7 @@ function redirectingServerListener(
820827
const { searchParams } = new URL(req.url ?? "", "http://localhost");
821828
const n = parseInt(searchParams.get("n") ?? "0");
822829
if (n > 0) {
823-
res.writeHead(302, { Location: `/?n=${n - 1}` });
830+
res.writeHead(302, { Location: `/?n=${n - 1}`, "Set-Cookie": `n=${n}` });
824831
} else {
825832
res.writeHead(200);
826833
}
@@ -889,6 +896,15 @@ test("fetch: removes Host and CF-Connecting-IP headers from Request", async (t)
889896
"x-real-ip": "127.0.0.1",
890897
});
891898
});
899+
test('fetch: returns full Response for "manual" redirect', async (t) => {
900+
const upstream = (await useServer(t, redirectingServerListener)).http;
901+
const url = new URL("/?n=3", upstream);
902+
const res = await fetch(url, { redirect: "manual" });
903+
t.is(res.status, 302);
904+
t.is(res.statusText, "Found");
905+
t.is(res.headers.get("Location"), `/?n=2`);
906+
t.is(res.headers.get("Set-Cookie"), "n=3");
907+
});
892908
test("fetch: waits for input gate to open before returning", async (t) => {
893909
const upstream = (await useServer(t, (req, res) => res.end("upstream"))).http;
894910
await waitsForInputGate(t, () => fetch(upstream));

packages/http-server/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ export async function convertNodeRequest(
130130
headers,
131131
body,
132132
cf: meta?.cf,
133+
// Incoming requests always have their redirect mode set to manual:
134+
// https://developers.cloudflare.com/workers/runtime-apis/request#requestinit
135+
redirect: "manual",
133136
});
134137
return { request, url };
135138
}

packages/http-server/test/index.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
triggerPromise,
3535
useMiniflare,
3636
useMiniflareWithHandler,
37+
useServer,
3738
useTmp,
3839
utf8Encode,
3940
} from "@miniflare/shared-test";
@@ -220,6 +221,10 @@ test("convertNodeRequest: includes cf object on request", async (t) => {
220221
t.not(req.cf, cf);
221222
t.deepEqual(req.cf, cf);
222223
});
224+
test('convertNodeRequest: defaults to "manual" redirect mode', async (t) => {
225+
const [req] = await buildConvertNodeRequest(t);
226+
t.is(req.redirect, "manual");
227+
});
223228

224229
test("createRequestListener: gets cf object from custom provider", async (t) => {
225230
const mf = useMiniflareWithHandler(
@@ -705,3 +710,31 @@ test("createServer: handles https requests", async (t) => {
705710
const [body] = await request(port, "", {}, true);
706711
t.is(body, `body:https://localhost:${port}/`);
707712
});
713+
test("createServer: proxies redirect responses", async (t) => {
714+
// https://github.com/cloudflare/miniflare/issues/133
715+
const upstream = await useServer(t, async (req, res) => {
716+
const { pathname } = new URL(req.url ?? "", "http://localhost");
717+
if (pathname === "/redirect") {
718+
t.is(await text(req), "body");
719+
res.writeHead(302, { Location: `/`, "Set-Cookie": `key=value` });
720+
} else {
721+
t.fail();
722+
}
723+
res.end();
724+
});
725+
const mf = useMiniflareWithHandler(
726+
{ HTTPPlugin, WebSocketPlugin },
727+
{ upstream: upstream.http.toString() },
728+
(globals, req) => globals.fetch(req)
729+
);
730+
const port = await listen(t, await createServer(mf));
731+
732+
const res = await new Promise<http.IncomingMessage>((resolve) =>
733+
http
734+
.request({ port, method: "POST", path: "/redirect" }, resolve)
735+
.end("body")
736+
);
737+
t.is(res.statusCode, 302);
738+
t.is(res.headers.location, `/`);
739+
t.deepEqual(res.headers["set-cookie"], ["key=value"]);
740+
});

0 commit comments

Comments
 (0)