Skip to content

Commit 335adc8

Browse files
author
theredandtheblue
committed
feat: redirect requests with repeated slashes
1 parent ae1afbb commit 335adc8

File tree

3 files changed

+38
-1
lines changed

3 files changed

+38
-1
lines changed

packages/open-next/src/core/routing/matcher.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
} from "types/next-types";
1111
import type { InternalEvent, InternalResult } from "types/open-next";
1212
import { emptyReadableStream, toReadableStream } from "utils/stream";
13+
import { normalizePath } from "utils/normalize-path"
1314

1415
import { debug } from "../../adapters/logger";
1516
import { handleLocaleRedirect, localizePath } from "./i18n";
@@ -262,6 +263,28 @@ export function handleRewrites<T extends RewriteDefinition>(
262263
};
263264
}
264265

266+
function handleRepeatedSlashRedirect(
267+
event: InternalEvent,
268+
): false | InternalResult {
269+
// Redirect `https://example.com//foo` to `https://example.com/foo`.
270+
const url = new URL(event.url);
271+
if (url.pathname.match(/(\\|\/\/)/)) {
272+
return {
273+
type: event.type,
274+
statusCode: 308,
275+
headers: {
276+
Location: `${url.protocol}//${url.host}${normalizePath(url.pathname)}${
277+
url.search
278+
}`,
279+
},
280+
body: emptyReadableStream(),
281+
isBase64Encoded: false,
282+
};
283+
}
284+
285+
return false;
286+
}
287+
265288
function handleTrailingSlashRedirect(
266289
event: InternalEvent,
267290
): false | InternalResult {
@@ -326,6 +349,9 @@ export function handleRedirects(
326349
event: InternalEvent,
327350
redirects: RedirectDefinition[],
328351
): InternalResult | undefined {
352+
const repeatedSlashRedirect = handleRepeatedSlashRedirect(event);
353+
if (repeatedSlashRedirect) return repeatedSlashRedirect;
354+
329355
const trailingSlashRedirect = handleTrailingSlashRedirect(event);
330356
if (trailingSlashRedirect) return trailingSlashRedirect;
331357

packages/open-next/src/utils/normalize-path.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import path from "node:path";
22

33
export function normalizePath(path: string) {
4-
return path.replace(/\\/g, "/");
4+
return path.replace(/\\/g, "/").replace(/\/\/+/g, "/");
55
}
66

77
export function getMonorepoRelativePath(relativePath = "../.."): string {

packages/tests-unit/tests/core/routing/matcher.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,17 @@ describe("getNextConfigHeaders", () => {
271271
});
272272

273273
describe("handleRedirects", () => {
274+
it("should redirect repeated slashes", () => {
275+
const event = createEvent({
276+
url: "https://on/api-route//foo",
277+
});
278+
279+
const result = handleRedirects(event, []);
280+
281+
expect(result.statusCode).toEqual(308);
282+
expect(result.headers.Location).toEqual("https://on/api-route/foo");
283+
});
284+
274285
it("should redirect trailing slash by default", () => {
275286
const event = createEvent({
276287
url: "https://on/api-route/",

0 commit comments

Comments
 (0)