Skip to content

Commit 7bb4fbe

Browse files
committed
Fix adapter request creation when double slashes exist
1 parent 518009d commit 7bb4fbe

File tree

9 files changed

+173
-6
lines changed

9 files changed

+173
-6
lines changed

.changeset/nine-apricots-breathe.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@remix-run/architect": patch
3+
"@remix-run/express": patch
4+
"@remix-run/netlify": patch
5+
"@remix-run/vercel": patch
6+
---
7+
8+
Fix Fetch Request creation for incoming URLs with double slashes

packages/remix-architect/__tests__/server-test.ts

+32
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,38 @@ describe("architect createRequestHandler", () => {
101101
});
102102
});
103103

104+
it("handles root // requests", async () => {
105+
mockedCreateRequestHandler.mockImplementation(() => async (req) => {
106+
return new Response(`URL: ${new URL(req.url).pathname}`);
107+
});
108+
109+
// We don't have a real app to test, but it doesn't matter. We won't ever
110+
// call through to the real createRequestHandler
111+
// @ts-expect-error
112+
await lambdaTester(createRequestHandler({ build: undefined }))
113+
.event(createMockEvent({ rawPath: "//" }))
114+
.expectResolve((res) => {
115+
expect(res.statusCode).toBe(200);
116+
expect(res.body).toBe("URL: //");
117+
});
118+
});
119+
120+
it("handles nested // requests", async () => {
121+
mockedCreateRequestHandler.mockImplementation(() => async (req) => {
122+
return new Response(`URL: ${new URL(req.url).pathname}`);
123+
});
124+
125+
// We don't have a real app to test, but it doesn't matter. We won't ever
126+
// call through to the real createRequestHandler
127+
// @ts-expect-error
128+
await lambdaTester(createRequestHandler({ build: undefined }))
129+
.event(createMockEvent({ rawPath: "//foo//bar" }))
130+
.expectResolve((res) => {
131+
expect(res.statusCode).toBe(200);
132+
expect(res.body).toBe("URL: //foo//bar");
133+
});
134+
});
135+
104136
it("handles null body", async () => {
105137
mockedCreateRequestHandler.mockImplementation(() => async () => {
106138
return new Response(null, { status: 200 });

packages/remix-architect/server.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function createRemixRequest(event: APIGatewayProxyEventV2): NodeRequest {
6262
let host = event.headers["x-forwarded-host"] || event.headers.host;
6363
let search = event.rawQueryString.length ? `?${event.rawQueryString}` : "";
6464
let scheme = process.env.ARC_SANDBOX ? "http" : "https";
65-
let url = new URL(event.rawPath + search, `${scheme}://${host}`);
65+
let url = new URL(`${scheme}://${host}${event.rawPath}${search}`);
6666
let isFormData = event.headers["content-type"]?.includes(
6767
"multipart/form-data"
6868
);

packages/remix-express/__tests__/server-test.ts

+24
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,30 @@ describe("express createRequestHandler", () => {
6464
expect(res.headers["x-powered-by"]).toBe("Express");
6565
});
6666

67+
it("handles root // URLs", async () => {
68+
mockedCreateRequestHandler.mockImplementation(() => async (req) => {
69+
return new Response("URL: " + new URL(req.url).pathname);
70+
});
71+
72+
let request = supertest(createApp());
73+
let res = await request.get("//");
74+
75+
expect(res.status).toBe(200);
76+
expect(res.text).toBe("URL: //");
77+
});
78+
79+
it("handles nested // URLs", async () => {
80+
mockedCreateRequestHandler.mockImplementation(() => async (req) => {
81+
return new Response("URL: " + new URL(req.url).pathname);
82+
});
83+
84+
let request = supertest(createApp());
85+
let res = await request.get("//foo//bar");
86+
87+
expect(res.status).toBe(200);
88+
expect(res.text).toBe("URL: //foo//bar");
89+
});
90+
6791
it("handles null body", async () => {
6892
mockedCreateRequestHandler.mockImplementation(() => async () => {
6993
return new Response(null, { status: 200 });

packages/remix-express/server.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,7 @@ export function createRemixRequest(
9393
req: express.Request,
9494
res: express.Response
9595
): NodeRequest {
96-
let origin = `${req.protocol}://${req.get("host")}`;
97-
let url = new URL(req.url, origin);
96+
let url = new URL(`${req.protocol}://${req.get("host")}${req.url}`);
9897

9998
// Abort action/loaders once we can no longer write a response
10099
let controller = new NodeAbortController();

packages/remix-netlify/__tests__/server-test.ts

+77-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ function createMockEvent(event: Partial<HandlerEvent> = {}): HandlerEvent {
3838
rawQuery: "",
3939
path: "/",
4040
httpMethod: "GET",
41-
headers: {},
41+
headers: {
42+
host: "localhost:3000",
43+
},
4244
multiValueHeaders: {},
4345
queryStringParameters: null,
4446
multiValueQueryStringParameters: null,
@@ -74,6 +76,80 @@ describe("netlify createRequestHandler", () => {
7476
});
7577
});
7678

79+
it("handles root // requests", async () => {
80+
mockedCreateRequestHandler.mockImplementation(() => async (req) => {
81+
return new Response(`URL: ${new URL(req.url).pathname}`);
82+
});
83+
84+
// We don't have a real app to test, but it doesn't matter. We won't ever
85+
// call through to the real createRequestHandler
86+
// @ts-expect-error
87+
await lambdaTester(createRequestHandler({ build: undefined }))
88+
.event(createMockEvent({ rawUrl: "http://localhost:3000//" }))
89+
.expectResolve((res) => {
90+
expect(res.statusCode).toBe(200);
91+
expect(res.body).toBe("URL: //");
92+
});
93+
});
94+
95+
it("handles nested // requests", async () => {
96+
mockedCreateRequestHandler.mockImplementation(() => async (req) => {
97+
return new Response(`URL: ${new URL(req.url).pathname}`);
98+
});
99+
100+
// We don't have a real app to test, but it doesn't matter. We won't ever
101+
// call through to the real createRequestHandler
102+
// @ts-expect-error
103+
await lambdaTester(createRequestHandler({ build: undefined }))
104+
.event(createMockEvent({ rawUrl: "http://localhost:3000//foo//bar" }))
105+
.expectResolve((res) => {
106+
expect(res.statusCode).toBe(200);
107+
expect(res.body).toBe("URL: //foo//bar");
108+
});
109+
});
110+
111+
it("handles root // requests (development)", async () => {
112+
let oldEnv = process.env.NODE_ENV;
113+
process.env.NODE_ENV = "development";
114+
115+
mockedCreateRequestHandler.mockImplementation(() => async (req) => {
116+
return new Response(`URL: ${new URL(req.url).pathname}`);
117+
});
118+
119+
// We don't have a real app to test, but it doesn't matter. We won't ever
120+
// call through to the real createRequestHandler
121+
// @ts-expect-error
122+
await lambdaTester(createRequestHandler({ build: undefined }))
123+
.event(createMockEvent({ path: "//" }))
124+
.expectResolve((res) => {
125+
expect(res.statusCode).toBe(200);
126+
expect(res.body).toBe("URL: //");
127+
});
128+
129+
process.env.NODE_ENV = oldEnv;
130+
});
131+
132+
it("handles nested // requests (development)", async () => {
133+
let oldEnv = process.env.NODE_ENV;
134+
process.env.NODE_ENV = "development";
135+
136+
mockedCreateRequestHandler.mockImplementation(() => async (req) => {
137+
return new Response(`URL: ${new URL(req.url).pathname}`);
138+
});
139+
140+
// We don't have a real app to test, but it doesn't matter. We won't ever
141+
// call through to the real createRequestHandler
142+
// @ts-expect-error
143+
await lambdaTester(createRequestHandler({ build: undefined }))
144+
.event(createMockEvent({ path: "//foo//bar" }))
145+
.expectResolve((res) => {
146+
expect(res.statusCode).toBe(200);
147+
expect(res.body).toBe("URL: //foo//bar");
148+
});
149+
150+
process.env.NODE_ENV = oldEnv;
151+
});
152+
77153
it("handles null body", async () => {
78154
mockedCreateRequestHandler.mockImplementation(() => async () => {
79155
return new Response(null, { status: 200 });

packages/remix-netlify/server.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function createRemixRequest(event: HandlerEvent): NodeRequest {
6363
} else {
6464
let origin = event.headers.host;
6565
let rawPath = getRawPath(event);
66-
url = new URL(rawPath, `http://${origin}`);
66+
url = new URL(`http://${origin}${rawPath}`);
6767
}
6868

6969
// Note: No current way to abort these for Netlify, but our router expects

packages/remix-vercel/__tests__/server-test.ts

+28
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,34 @@ describe("vercel createRequestHandler", () => {
6868
expect(res.text).toBe("URL: /foo/bar");
6969
});
7070

71+
it("handles root // requests", async () => {
72+
mockedCreateRequestHandler.mockImplementation(() => async (req) => {
73+
return new Response(`URL: ${new URL(req.url).pathname}`);
74+
});
75+
76+
let request = supertest(createApp());
77+
// note: vercel's createServerWithHelpers requires a x-now-bridge-request-id
78+
let res = await request.get("//").set({ "x-now-bridge-request-id": "2" });
79+
80+
expect(res.status).toBe(200);
81+
expect(res.text).toBe("URL: //");
82+
});
83+
84+
it("handles nested // requests", async () => {
85+
mockedCreateRequestHandler.mockImplementation(() => async (req) => {
86+
return new Response(`URL: ${new URL(req.url).pathname}`);
87+
});
88+
89+
let request = supertest(createApp());
90+
// note: vercel's createServerWithHelpers requires a x-now-bridge-request-id
91+
let res = await request
92+
.get("//foo//bar")
93+
.set({ "x-now-bridge-request-id": "2" });
94+
95+
expect(res.status).toBe(200);
96+
expect(res.text).toBe("URL: //foo//bar");
97+
});
98+
7199
it("handles null body", async () => {
72100
mockedCreateRequestHandler.mockImplementation(() => async () => {
73101
return new Response(null, { status: 200 });

packages/remix-vercel/server.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export function createRemixRequest(
8282
let host = req.headers["x-forwarded-host"] || req.headers["host"];
8383
// doesn't seem to be available on their req object!
8484
let protocol = req.headers["x-forwarded-proto"] || "https";
85-
let url = new URL(req.url!, `${protocol}://${host}`);
85+
let url = new URL(`${protocol}://${host}${req.url}`);
8686

8787
// Abort action/loaders once we can no longer write a response
8888
let controller = new NodeAbortController();

0 commit comments

Comments
 (0)