From 06abd634899095b6cc28e6e8315b1e8b9c8df939 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Wed, 7 Jun 2023 14:50:22 -0700 Subject: [PATCH] Add experimental caseSensitiveRoutes config (#50869) This adds an experimental `caseSensitiveRoutes` config that currently applies for `rewrites`, `redirects`, and `headers` to change the default of case-insensitive. x-ref: [slack thread](https://vercel.slack.com/archives/C02K2HCH5V4/p1686080359514479?thread_ts=1686077053.623389&cid=C02K2HCH5V4) x-ref: [slack thread](https://vercel.slack.com/archives/C057RG6Q9MX/p1686078875948069?thread_ts=1686077882.133609&cid=C057RG6Q9MX) x-ref: https://github.com/vercel/next.js/issues/21498 --- packages/next/src/build/index.ts | 2 + packages/next/src/server/base-server.ts | 1 + packages/next/src/server/config-schema.ts | 3 ++ packages/next/src/server/config-shared.ts | 2 + packages/next/src/server/next-server.ts | 15 ++++++- .../next/src/server/server-route-utils.ts | 11 ++++++ packages/next/src/server/server-utils.ts | 3 ++ .../src/shared/lib/router/utils/path-match.ts | 8 +++- test/integration/custom-routes/next.config.js | 3 ++ .../custom-routes/test/index.test.js | 39 +++++++++++++++++++ .../dynamic-routing/test/index.test.js | 1 + test/lib/next-test-utils.js | 2 +- 12 files changed, 86 insertions(+), 4 deletions(-) diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index d3d0d39625a79..ba1fd3843e828 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -712,6 +712,7 @@ export default async function build( varyHeader: typeof RSC_VARY_HEADER } skipMiddlewareUrlNormalize?: boolean + caseSensitive?: boolean } = nextBuildSpan.traceChild('generate-routes-manifest').traceFn(() => { const sortedRoutes = getSortedRoutes([ ...pageKeys.pages, @@ -731,6 +732,7 @@ export default async function build( return { version: 3, pages404: true, + caseSensitive: !!config.experimental.caseSensitiveRoutes, basePath: config.basePath, redirects: redirects.map((r: any) => buildCustomRoute(r, 'redirect')), headers: headers.map((r: any) => buildCustomRoute(r, 'header')), diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 09929738a0fa9..48e69bc4ac93f 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -783,6 +783,7 @@ export default abstract class Server { i18n: this.nextConfig.i18n, basePath: this.nextConfig.basePath, rewrites: this.customRoutes.rewrites, + caseSensitive: !!this.nextConfig.experimental.caseSensitiveRoutes, }) // Ensure parsedUrl.pathname includes locale before processing diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index d48a410609520..2ddf822473916 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -270,6 +270,9 @@ const configSchema = { craCompat: { type: 'boolean', }, + caseSensitiveRoutes: { + type: 'boolean', + }, useDeploymentId: { type: 'boolean', }, diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 5987dfd7c3d85..39d0a19ef1506 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -144,6 +144,7 @@ export interface NextJsWebpackConfig { } export interface ExperimentalConfig { + caseSensitiveRoutes?: boolean useDeploymentId?: boolean useDeploymentIdServerActions?: boolean deploymentId?: string @@ -663,6 +664,7 @@ export const defaultConfig: NextConfig = { output: !!process.env.NEXT_PRIVATE_STANDALONE ? 'standalone' : undefined, modularizeImports: undefined, experimental: { + caseSensitiveRoutes: false, useDeploymentId: false, deploymentId: undefined, useDeploymentIdServerActions: false, diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index 4be889084b9ce..67b9b5bbb8baf 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -1335,6 +1335,8 @@ export default class NextNodeServer extends BaseServer { ...publicRoutes, ...staticFilesRoutes, ] + const caseSensitiveRoutes = + !!this.nextConfig.experimental.caseSensitiveRoutes const restrictedRedirectPaths = this.nextConfig.basePath ? [`${this.nextConfig.basePath}/_next`] @@ -1345,14 +1347,22 @@ export default class NextNodeServer extends BaseServer { this.minimalMode || this.isRenderWorker ? [] : this.customRoutes.headers.map((rule) => - createHeaderRoute({ rule, restrictedRedirectPaths }) + createHeaderRoute({ + rule, + restrictedRedirectPaths, + caseSensitive: caseSensitiveRoutes, + }) ) const redirects = this.minimalMode || this.isRenderWorker ? [] : this.customRoutes.redirects.map((rule) => - createRedirectRoute({ rule, restrictedRedirectPaths }) + createRedirectRoute({ + rule, + restrictedRedirectPaths, + caseSensitive: caseSensitiveRoutes, + }) ) const rewrites = this.generateRewrites({ restrictedRedirectPaths }) @@ -2044,6 +2054,7 @@ export default class NextNodeServer extends BaseServer { type: 'rewrite', rule: rewrite, restrictedRedirectPaths, + caseSensitive: !!this.nextConfig.experimental.caseSensitiveRoutes, }) return { ...rewriteRoute, diff --git a/packages/next/src/server/server-route-utils.ts b/packages/next/src/server/server-route-utils.ts index 9cef9bf532cd8..7a3a086594a8a 100644 --- a/packages/next/src/server/server-route-utils.ts +++ b/packages/next/src/server/server-route-utils.ts @@ -24,21 +24,25 @@ export function getCustomRoute(params: { rule: Header type: RouteType restrictedRedirectPaths: string[] + caseSensitive: boolean }): Route & Header export function getCustomRoute(params: { rule: Rewrite type: RouteType restrictedRedirectPaths: string[] + caseSensitive: boolean }): Route & Rewrite export function getCustomRoute(params: { rule: Redirect type: RouteType restrictedRedirectPaths: string[] + caseSensitive: boolean }): Route & Redirect export function getCustomRoute(params: { rule: Rewrite | Redirect | Header type: RouteType restrictedRedirectPaths: string[] + caseSensitive: boolean }): (Route & Rewrite) | (Route & Header) | (Route & Rewrite) { const { rule, type, restrictedRedirectPaths } = params const match = getPathMatch(rule.source, { @@ -51,6 +55,7 @@ export function getCustomRoute(params: { type === 'redirect' ? restrictedRedirectPaths : undefined ) : undefined, + sensitive: params.caseSensitive, }) return { @@ -65,14 +70,17 @@ export function getCustomRoute(params: { export const createHeaderRoute = ({ rule, restrictedRedirectPaths, + caseSensitive, }: { rule: Header restrictedRedirectPaths: string[] + caseSensitive: boolean }): Route => { const headerRoute = getCustomRoute({ type: 'header', rule, restrictedRedirectPaths, + caseSensitive, }) return { match: headerRoute.match, @@ -134,14 +142,17 @@ export const stringifyQuery = (req: BaseNextRequest, query: ParsedUrlQuery) => { export const createRedirectRoute = ({ rule, restrictedRedirectPaths, + caseSensitive, }: { rule: Redirect restrictedRedirectPaths: string[] + caseSensitive: boolean }): Route => { const redirectRoute = getCustomRoute({ type: 'redirect', rule, restrictedRedirectPaths, + caseSensitive, }) return { internal: redirectRoute.internal, diff --git a/packages/next/src/server/server-utils.ts b/packages/next/src/server/server-utils.ts index 8ac09442697b5..336b5d6aaaec1 100644 --- a/packages/next/src/server/server-utils.ts +++ b/packages/next/src/server/server-utils.ts @@ -97,6 +97,7 @@ export function getUtils({ rewrites, pageIsDynamic, trailingSlash, + caseSensitive, }: { page: string i18n?: NextConfig['i18n'] @@ -108,6 +109,7 @@ export function getUtils({ } pageIsDynamic: boolean trailingSlash?: boolean + caseSensitive: boolean }) { let defaultRouteRegex: ReturnType | undefined let dynamicRouteMatcher: RouteMatchFn | undefined @@ -140,6 +142,7 @@ export function getUtils({ { removeUnnamedParams: true, strict: true, + sensitive: !!caseSensitive, } ) let params = matcher(parsedUrl.pathname) diff --git a/packages/next/src/shared/lib/router/utils/path-match.ts b/packages/next/src/shared/lib/router/utils/path-match.ts index 712d6d0cd3132..003962c57fd1a 100644 --- a/packages/next/src/shared/lib/router/utils/path-match.ts +++ b/packages/next/src/shared/lib/router/utils/path-match.ts @@ -18,6 +18,11 @@ interface Options { * to match. */ strict?: boolean + + /** + * When true the matcher will be case-sensitive, defaults to false + */ + sensitive?: boolean } /** @@ -29,7 +34,8 @@ export function getPathMatch(path: string, options?: Options) { const keys: Key[] = [] const regexp = pathToRegexp(path, keys, { delimiter: '/', - sensitive: false, + sensitive: + typeof options?.sensitive === 'boolean' ? options.sensitive : false, strict: options?.strict, }) diff --git a/test/integration/custom-routes/next.config.js b/test/integration/custom-routes/next.config.js index b484e588fa3d5..58a057f9983be 100644 --- a/test/integration/custom-routes/next.config.js +++ b/test/integration/custom-routes/next.config.js @@ -1,4 +1,7 @@ module.exports = { + experimental: { + caseSensitiveRoutes: true, + }, async rewrites() { // no-rewrites comment return { diff --git a/test/integration/custom-routes/test/index.test.js b/test/integration/custom-routes/test/index.test.js index 775a21abd5b54..f777ee74622a7 100644 --- a/test/integration/custom-routes/test/index.test.js +++ b/test/integration/custom-routes/test/index.test.js @@ -39,6 +39,44 @@ let appPort let app const runTests = (isDev = false, isTurbo = false) => { + it.each([ + { + path: '/to-ANOTHER', + content: /could not be found/, + status: 404, + }, + { + path: '/HELLO-world', + content: /could not be found/, + status: 404, + }, + { + path: '/docs/GITHUB', + content: /could not be found/, + status: 404, + }, + { + path: '/add-HEADER', + content: /could not be found/, + status: 404, + }, + ])( + 'should honor caseSensitiveRoutes config for $path', + async ({ path, status, content }) => { + const res = await fetchViaHTTP(appPort, path, undefined, { + redirect: 'manual', + }) + + if (status) { + expect(res.status).toBe(status) + } + + if (content) { + expect(await res.text()).toMatch(content) + } + } + ) + it('should successfully rewrite a WebSocket request', async () => { // TODO: remove once test failure has been fixed if (isTurbo) return @@ -1534,6 +1572,7 @@ const runTests = (isDev = false, isTurbo = false) => { expect(manifest).toEqual({ version: 3, pages404: true, + caseSensitive: true, basePath: '', dataRoutes: [ { diff --git a/test/integration/dynamic-routing/test/index.test.js b/test/integration/dynamic-routing/test/index.test.js index cf4fbe95420bb..2699e64d2bc6e 100644 --- a/test/integration/dynamic-routing/test/index.test.js +++ b/test/integration/dynamic-routing/test/index.test.js @@ -1232,6 +1232,7 @@ function runTests({ dev }) { expect(manifest).toEqual({ version: 3, pages404: true, + caseSensitive: false, basePath: '', headers: [], rewrites: [], diff --git a/test/lib/next-test-utils.js b/test/lib/next-test-utils.js index d5a18ba1b38ab..dc4fc62ca02c9 100644 --- a/test/lib/next-test-utils.js +++ b/test/lib/next-test-utils.js @@ -154,7 +154,7 @@ export function renderViaHTTP(appPort, pathname, query, opts) { * @param {string} pathname * @param {Record | string | null | undefined} [query] * @param {import('node-fetch').RequestInit} [opts] - * @returns {Promise} + * @returns {Promise} */ export function fetchViaHTTP(appPort, pathname, query, opts) { const url = query ? withQuery(pathname, query) : pathname