Skip to content

Commit

Permalink
Resolve redirect URLs repeatedly
Browse files Browse the repository at this point in the history
  • Loading branch information
blakeembrey committed Mar 9, 2022
1 parent 3d3a3db commit 1e6bc4d
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 24 deletions.
73 changes: 73 additions & 0 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,40 @@ describe("popsicle redirects", () => {
expect(spy).toHaveBeenCalledTimes(2);
});

it("should resolve based on request url", async () => {
const spy = jest.fn(async (req: Request) => {
if (spy.mock.calls.length === 1) {
return new Response(null, {
status: 302,
headers: {
Location: "http://foobar.com",
},
});
}

if (spy.mock.calls.length === 2) {
return new Response(null, {
status: 302,
headers: {
Location: "/test",
},
});
}

expect(req.url).toEqual("http://foobar.com/test");
return new Response(null, { status: 200 });
});

const transport = redirects(spy);

const res = await transport(new Request("http://example.com"), async () => {
throw new TypeError("Unexpected response");
});

expect(res.status).toEqual(200);
expect(spy).toHaveBeenCalledTimes(3);
});

describe("secure headers", () => {
const headers = {
cookie: "example_cookie",
Expand Down Expand Up @@ -93,5 +127,44 @@ describe("popsicle redirects", () => {
expect(res.status).toEqual(200);
expect(spy).toHaveBeenCalledTimes(2);
});

it("should discard cookies when off the original host", async () => {
const spy = jest.fn(async (req: Request) => {
if (spy.mock.calls.length === 1) {
return new Response(null, {
status: 302,
headers: {
Location: "https://example.com",
},
});
}

if (spy.mock.calls.length === 2) {
return new Response(null, {
status: 302,
headers: {
Location: "/test",
},
});
}

expect(req.url).toEqual("https://example.com/test");
expect(req.headers.get("Cookie")).toBe(null);
expect(req.headers.get("Authorization")).toBe(null);
return new Response(null, { status: 200 });
});

const transport = redirects(spy);

const res = await transport(
new Request("http://example.com", { headers }),
async () => {
throw new TypeError("Unexpected response");
}
);

expect(res.status).toEqual(200);
expect(spy).toHaveBeenCalledTimes(3);
});
});
});
47 changes: 23 additions & 24 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,28 +43,26 @@ export class MaxRedirectsError extends Error {
/**
* Create a new request object and tidy up any loose ends to avoid leaking info.
*/
function safeRedirect<T>(
request: CommonRequest<T>,
location: string,
method: string
) {
const originalUrl = new URL(request.url);
const newUrl = new URL(location, originalUrl);

const url = newUrl.toString();
request.signal.emit("redirect", url);

const newRequest = request.clone();
newRequest.url = url;
newRequest.method = method;

// Delete cookie header when leaving the original URL.
if (originalUrl.origin !== newUrl.origin) {
newRequest.headers.delete("cookie");
newRequest.headers.delete("authorization");
}
function safeRedirect<T>(initReq: CommonRequest<T>) {
const originalUrl = new URL(initReq.url);

return (req: CommonRequest<T>, location: string, method: string) => {
const newUrl = new URL(location, req.url);

req.signal.emit("redirect", newUrl.toString());

const newRequest = initReq.clone();
newRequest.url = newUrl.toString();
newRequest.method = method;

return newRequest;
// Delete cookie header when leaving the original URL.
if (newUrl.origin !== originalUrl.origin) {
newRequest.headers.delete("cookie");
newRequest.headers.delete("authorization");
}

return newRequest;
};
}

/**
Expand All @@ -87,6 +85,7 @@ export function redirects<T extends CommonRequest, U extends CommonResponse>(
confirmRedirect: ConfirmRedirect = () => false
): (req: T, next: () => Promise<U>) => Promise<U> {
return async function (initReq, done) {
const safeClone = safeRedirect(initReq);
let req = initReq.clone();
let redirectCount = 0;

Expand All @@ -102,7 +101,7 @@ export function redirects<T extends CommonRequest, U extends CommonResponse>(
if (redirect === REDIRECT_TYPE.FOLLOW_WITH_GET) {
const method = initReq.method.toUpperCase() === "HEAD" ? "HEAD" : "GET";

req = safeRedirect(initReq, location, method);
req = safeClone(req, location, method);
req.$rawBody = null; // Override internal raw body.
req.headers.set("Content-Length", "0");

Expand All @@ -114,14 +113,14 @@ export function redirects<T extends CommonRequest, U extends CommonResponse>(

// Following HTTP spec by automatically redirecting with GET/HEAD.
if (method.toUpperCase() === "GET" || method.toUpperCase() === "HEAD") {
req = safeRedirect(initReq, location, method);
req = safeClone(req, location, method);

continue;
}

// Allow the user to confirm redirect according to HTTP spec.
if (confirmRedirect(req, res)) {
req = safeRedirect(initReq, location, method);
req = safeClone(req, location, method);

continue;
}
Expand Down

0 comments on commit 1e6bc4d

Please sign in to comment.