From 8c3c2b7ea2c6ee5cfaeceae1137c2c352bf66dfa Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 13 Jul 2021 14:38:14 -0500 Subject: [PATCH] Update redirect regexes to not match _next (#27143) This updates redirects' regexes to not match `/_next` paths since this is currently unexpected and can easily cause a multi-match redirect to break loading client-side assets. This also fixes custom-routes not matching correctly when `trailingSlash: true/false` is used ## Bug - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [x] Errors have helpful link attached, see `contributing.md` x-ref: https://github.com/vercel/next.js/discussions/24683 x-ref: [slack thread](https://vercel.slack.com/archives/CGU8HUTUH/p1626159845474000) --- packages/next/build/index.ts | 15 +- packages/next/lib/load-custom-routes.ts | 24 +- packages/next/server/next-server.ts | 16 +- .../shared/lib/router/utils/path-match.ts | 10 +- test/integration/custom-routes/next.config.js | 11 + .../custom-routes/test/index.test.js | 210 +++++++++++------- 6 files changed, 205 insertions(+), 81 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 6666e4f035489..b93335a2f91a5 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -19,6 +19,7 @@ import { findPagesDir } from '../lib/find-pages-dir' import loadCustomRoutes, { CustomRoutes, getRedirectStatus, + modifyRouteRegex, normalizeRouteRegex, Redirect, Rewrite, @@ -330,6 +331,10 @@ export default async function build( ) } + const restrictedRedirectPaths = ['/_next'].map((p) => + config.basePath ? `${config.basePath}${p}` : p + ) + const buildCustomRoute = ( r: { source: string @@ -347,6 +352,14 @@ export default async function build( sensitive: false, delimiter: '/', // default is `/#?`, but Next does not pass query info }) + let regexSource = routeRegex.source + + if (!(r as any).internal) { + regexSource = modifyRouteRegex( + routeRegex.source, + type === 'redirect' ? restrictedRedirectPaths : undefined + ) + } return { ...r, @@ -356,7 +369,7 @@ export default async function build( permanent: undefined, } : {}), - regex: normalizeRouteRegex(routeRegex.source), + regex: normalizeRouteRegex(regexSource), } } diff --git a/packages/next/lib/load-custom-routes.ts b/packages/next/lib/load-custom-routes.ts index 92ccf2fd6b309..ca4240bcf798b 100644 --- a/packages/next/lib/load-custom-routes.ts +++ b/packages/next/lib/load-custom-routes.ts @@ -65,6 +65,22 @@ export function normalizeRouteRegex(regex: string) { return regex.replace(/\\\//g, '/') } +// for redirects we restrict matching /_next and for all routes +// we add an optional trailing slash at the end for easier +// configuring between trailingSlash: true/false +export function modifyRouteRegex(regex: string, restrictedPaths?: string[]) { + if (restrictedPaths) { + regex = regex.replace( + /\^/, + `^(?!${restrictedPaths + .map((path) => path.replace(/\//g, '\\/')) + .join('|')})` + ) + } + regex = regex.replace(/\$$/, '(?:\\/)?$') + return regex +} + function checkRedirect( route: Redirect ): { invalidParts: string[]; hadInvalidStatus: boolean } { @@ -525,10 +541,14 @@ function processRoutes( }` } } - r.source = `${srcBasePath}${r.source}` + r.source = `${srcBasePath}${ + r.source === '/' && srcBasePath ? '' : r.source + }` if (r.destination) { - r.destination = `${destBasePath}${r.destination}` + r.destination = `${destBasePath}${ + r.destination === '/' && destBasePath ? '' : r.destination + }` } newRoutes.push(r) } diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index c6a3d5377cecd..26afcff6d8b6d 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -18,6 +18,7 @@ import { Rewrite, RouteType, CustomRoutes, + modifyRouteRegex, } from '../lib/load-custom-routes' import { BUILD_ID_FILE, @@ -822,11 +823,24 @@ export default class Server { ...staticFilesRoute, ] + const restrictedRedirectPaths = ['/_next'].map((p) => + this.nextConfig.basePath ? `${this.nextConfig.basePath}${p}` : p + ) + const getCustomRoute = ( r: Rewrite | Redirect | Header, type: RouteType ) => { - const match = getCustomRouteMatcher(r.source) + const match = getCustomRouteMatcher( + r.source, + !(r as any).internal + ? (regex: string) => + modifyRouteRegex( + regex, + type === 'redirect' ? restrictedRedirectPaths : undefined + ) + : undefined + ) return { ...r, diff --git a/packages/next/shared/lib/router/utils/path-match.ts b/packages/next/shared/lib/router/utils/path-match.ts index 8633a35cc5fb4..784c082927bb0 100644 --- a/packages/next/shared/lib/router/utils/path-match.ts +++ b/packages/next/shared/lib/router/utils/path-match.ts @@ -15,13 +15,19 @@ export const customRouteMatcherOptions: pathToRegexp.TokensToRegexpOptions & } export default (customRoute = false) => { - return (path: string) => { + return (path: string, regexModifier?: (regex: string) => string) => { const keys: pathToRegexp.Key[] = [] - const matcherRegex = pathToRegexp.pathToRegexp( + let matcherRegex = pathToRegexp.pathToRegexp( path, keys, customRoute ? customRouteMatcherOptions : matcherOptions ) + + if (regexModifier) { + const regexSource = regexModifier(matcherRegex.source) + matcherRegex = new RegExp(regexSource, matcherRegex.flags) + } + const matcher = pathToRegexp.regexpToFunction(matcherRegex, keys) return (pathname: string | null | undefined, params?: any) => { diff --git a/test/integration/custom-routes/next.config.js b/test/integration/custom-routes/next.config.js index 9e3a33cf34608..ff6d31628af4c 100644 --- a/test/integration/custom-routes/next.config.js +++ b/test/integration/custom-routes/next.config.js @@ -378,6 +378,17 @@ module.exports = { destination: '/another?host=1', permanent: false, }, + { + source: '/:path/has-redirect-5', + has: [ + { + type: 'header', + key: 'x-test-next', + }, + ], + destination: '/somewhere', + permanent: false, + }, ] }, diff --git a/test/integration/custom-routes/test/index.test.js b/test/integration/custom-routes/test/index.test.js index 023a10fb27784..17d77a3faa5aa 100644 --- a/test/integration/custom-routes/test/index.test.js +++ b/test/integration/custom-routes/test/index.test.js @@ -233,6 +233,34 @@ const runTests = (isDev = false) => { expect(res3location).toBe('/') }) + it('should not match redirect for /_next', async () => { + const res = await fetchViaHTTP( + appPort, + '/_next/has-redirect-5', + undefined, + { + headers: { + 'x-test-next': 'true', + }, + redirect: 'manual', + } + ) + expect(res.status).toBe(404) + + const res2 = await fetchViaHTTP( + appPort, + '/another/has-redirect-5', + undefined, + { + headers: { + 'x-test-next': 'true', + }, + redirect: 'manual', + } + ) + expect(res2.status).toBe(307) + }) + it('should redirect successfully with permanent: false', async () => { const res = await fetchViaHTTP(appPort, '/redirect1', undefined, { redirect: 'manual', @@ -1096,7 +1124,7 @@ const runTests = (isDev = false) => { { destination: '/:lang/about', regex: normalizeRegEx( - '^\\/redirect\\/me\\/to-about(?:\\/([^\\/]+?))$' + '^(?!\\/_next)\\/redirect\\/me\\/to-about(?:\\/([^\\/]+?))(?:\\/)?$' ), source: '/redirect/me/to-about/:lang', statusCode: 307, @@ -1105,78 +1133,84 @@ const runTests = (isDev = false) => { source: '/docs/router-status/:code', destination: '/docs/v2/network/status-codes#:code', statusCode: 301, - regex: normalizeRegEx('^\\/docs\\/router-status(?:\\/([^\\/]+?))$'), + regex: normalizeRegEx( + '^(?!\\/_next)\\/docs\\/router-status(?:\\/([^\\/]+?))(?:\\/)?$' + ), }, { source: '/docs/github', destination: '/docs/v2/advanced/now-for-github', statusCode: 301, - regex: normalizeRegEx('^\\/docs\\/github$'), + regex: normalizeRegEx('^(?!\\/_next)\\/docs\\/github(?:\\/)?$'), }, { source: '/docs/v2/advanced/:all(.*)', destination: '/docs/v2/more/:all', statusCode: 301, - regex: normalizeRegEx('^\\/docs\\/v2\\/advanced(?:\\/(.*))$'), + regex: normalizeRegEx( + '^(?!\\/_next)\\/docs\\/v2\\/advanced(?:\\/(.*))(?:\\/)?$' + ), }, { source: '/hello/:id/another', destination: '/blog/:id', statusCode: 307, - regex: normalizeRegEx('^\\/hello(?:\\/([^\\/]+?))\\/another$'), + regex: normalizeRegEx( + '^(?!\\/_next)\\/hello(?:\\/([^\\/]+?))\\/another(?:\\/)?$' + ), }, { source: '/redirect1', destination: '/', statusCode: 307, - regex: normalizeRegEx('^\\/redirect1$'), + regex: normalizeRegEx('^(?!\\/_next)\\/redirect1(?:\\/)?$'), }, { source: '/redirect2', destination: '/', statusCode: 301, - regex: normalizeRegEx('^\\/redirect2$'), + regex: normalizeRegEx('^(?!\\/_next)\\/redirect2(?:\\/)?$'), }, { source: '/redirect3', destination: '/another', statusCode: 302, - regex: normalizeRegEx('^\\/redirect3$'), + regex: normalizeRegEx('^(?!\\/_next)\\/redirect3(?:\\/)?$'), }, { source: '/redirect4', destination: '/', statusCode: 308, - regex: normalizeRegEx('^\\/redirect4$'), + regex: normalizeRegEx('^(?!\\/_next)\\/redirect4(?:\\/)?$'), }, { source: '/redir-chain1', destination: '/redir-chain2', statusCode: 301, - regex: normalizeRegEx('^\\/redir-chain1$'), + regex: normalizeRegEx('^(?!\\/_next)\\/redir-chain1(?:\\/)?$'), }, { source: '/redir-chain2', destination: '/redir-chain3', statusCode: 302, - regex: normalizeRegEx('^\\/redir-chain2$'), + regex: normalizeRegEx('^(?!\\/_next)\\/redir-chain2(?:\\/)?$'), }, { source: '/redir-chain3', destination: '/', statusCode: 303, - regex: normalizeRegEx('^\\/redir-chain3$'), + regex: normalizeRegEx('^(?!\\/_next)\\/redir-chain3(?:\\/)?$'), }, { destination: 'https://google.com', - regex: normalizeRegEx('^\\/to-external$'), + regex: normalizeRegEx('^(?!\\/_next)\\/to-external(?:\\/)?$'), source: '/to-external', statusCode: 307, }, { destination: '/with-params?first=:section&second=:name', regex: normalizeRegEx( - '^\\/query-redirect(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))$' + '^(?!\\/_next)\\/query-redirect(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))(?:\\/)?$' ), source: '/query-redirect/:section/:name', statusCode: 307, @@ -1184,27 +1218,29 @@ const runTests = (isDev = false) => { { destination: '/got-unnamed', regex: normalizeRegEx( - '^\\/unnamed(?:\\/(first|second))(?:\\/(.*))$' + '^(?!\\/_next)\\/unnamed(?:\\/(first|second))(?:\\/(.*))(?:\\/)?$' ), source: '/unnamed/(first|second)/(.*)', statusCode: 307, }, { destination: '/:0', - regex: normalizeRegEx('^\\/named-like-unnamed(?:\\/([^\\/]+?))$'), + regex: normalizeRegEx( + '^(?!\\/_next)\\/named-like-unnamed(?:\\/([^\\/]+?))(?:\\/)?$' + ), source: '/named-like-unnamed/:0', statusCode: 307, }, { destination: '/thank-you-next', - regex: normalizeRegEx('^\\/redirect-override$'), + regex: normalizeRegEx('^(?!\\/_next)\\/redirect-override(?:\\/)?$'), source: '/redirect-override', statusCode: 307, }, { destination: '/:first/:second', regex: normalizeRegEx( - '^\\/docs(?:\\/(integrations|now-cli))\\/v2(.*)$' + '^(?!\\/_next)\\/docs(?:\\/(integrations|now-cli))\\/v2(.*)(?:\\/)?$' ), source: '/docs/:first(integrations|now-cli)/v2:second(.*)', statusCode: 307, @@ -1212,7 +1248,7 @@ const runTests = (isDev = false) => { { destination: '/somewhere', regex: normalizeRegEx( - '^\\/catchall-redirect(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?$' + '^(?!\\/_next)\\/catchall-redirect(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$' ), source: '/catchall-redirect/:path*', statusCode: 307, @@ -1220,14 +1256,18 @@ const runTests = (isDev = false) => { { destination: 'https://authserver.example.com/set-password?returnUrl=https%3A%2F%2Fwww.example.com/login', - regex: normalizeRegEx('^\\/to-external-with-query$'), + regex: normalizeRegEx( + '^(?!\\/_next)\\/to-external-with-query(?:\\/)?$' + ), source: '/to-external-with-query', statusCode: 307, }, { destination: 'https://authserver.example.com/set-password?returnUrl=https://www.example.com/login', - regex: normalizeRegEx('^\\/to-external-with-query-2$'), + regex: normalizeRegEx( + '^(?!\\/_next)\\/to-external-with-query-2(?:\\/)?$' + ), source: '/to-external-with-query-2', statusCode: 307, }, @@ -1240,7 +1280,7 @@ const runTests = (isDev = false) => { value: '(?.*)', }, ], - regex: normalizeRegEx('^\\/has-redirect-1$'), + regex: normalizeRegEx('^(?!\\/_next)\\/has-redirect-1(?:\\/)?$'), source: '/has-redirect-1', statusCode: 307, }, @@ -1252,7 +1292,7 @@ const runTests = (isDev = false) => { type: 'query', }, ], - regex: normalizeRegEx('^\\/has-redirect-2$'), + regex: normalizeRegEx('^(?!\\/_next)\\/has-redirect-2(?:\\/)?$'), source: '/has-redirect-2', statusCode: 307, }, @@ -1265,7 +1305,7 @@ const runTests = (isDev = false) => { value: 'true', }, ], - regex: normalizeRegEx('^\\/has-redirect-3$'), + regex: normalizeRegEx('^(?!\\/_next)\\/has-redirect-3(?:\\/)?$'), source: '/has-redirect-3', statusCode: 307, }, @@ -1277,10 +1317,24 @@ const runTests = (isDev = false) => { value: 'example.com', }, ], - regex: normalizeRegEx('^\\/has-redirect-4$'), + regex: normalizeRegEx('^(?!\\/_next)\\/has-redirect-4(?:\\/)?$'), source: '/has-redirect-4', statusCode: 307, }, + { + destination: '/somewhere', + has: [ + { + key: 'x-test-next', + type: 'header', + }, + ], + regex: normalizeRegEx( + '^(?!\\/_next)(?:\\/([^\\/]+?))\\/has-redirect-5(?:\\/)?$' + ), + source: '/:path/has-redirect-5', + statusCode: 307, + }, ], headers: [ { @@ -1294,7 +1348,7 @@ const runTests = (isDev = false) => { value: 'hello again', }, ], - regex: normalizeRegEx('^\\/add-header$'), + regex: normalizeRegEx('^\\/add-header(?:\\/)?$'), source: '/add-header', }, { @@ -1308,7 +1362,7 @@ const runTests = (isDev = false) => { value: 'second', }, ], - regex: normalizeRegEx('^\\/my-headers(?:\\/(.*))$'), + regex: normalizeRegEx('^\\/my-headers(?:\\/(.*))(?:\\/)?$'), source: '/my-headers/(.*)', }, { @@ -1363,7 +1417,9 @@ const runTests = (isDev = false) => { "default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com/:path", }, ], - regex: normalizeRegEx('^\\/my-other-header(?:\\/([^\\/]+?))$'), + regex: normalizeRegEx( + '^\\/my-other-header(?:\\/([^\\/]+?))(?:\\/)?$' + ), source: '/my-other-header/:path', }, { @@ -1373,7 +1429,7 @@ const runTests = (isDev = false) => { value: 'https://example.com', }, ], - regex: normalizeRegEx('^\\/without-params\\/url$'), + regex: normalizeRegEx('^\\/without-params\\/url(?:\\/)?$'), source: '/without-params/url', }, { @@ -1384,7 +1440,7 @@ const runTests = (isDev = false) => { }, ], regex: normalizeRegEx( - '^\\/with-params\\/url(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?$' + '^\\/with-params\\/url(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$' ), source: '/with-params/url/:path*', }, @@ -1396,7 +1452,7 @@ const runTests = (isDev = false) => { }, ], regex: normalizeRegEx( - '^\\/with-params\\/url2(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?$' + '^\\/with-params\\/url2(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$' ), source: '/with-params/url2/:path*', }, @@ -1408,7 +1464,7 @@ const runTests = (isDev = false) => { }, ], regex: normalizeRegEx( - '^(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?$' + '^(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$' ), source: '/:path*', }, @@ -1423,7 +1479,7 @@ const runTests = (isDev = false) => { value: 'end', }, ], - regex: normalizeRegEx('^\\/named-pattern(?:\\/(.*))$'), + regex: normalizeRegEx('^\\/named-pattern(?:\\/(.*))(?:\\/)?$'), source: '/named-pattern/:path(.*)', }, { @@ -1434,7 +1490,7 @@ const runTests = (isDev = false) => { }, ], regex: normalizeRegEx( - '^\\/catchall-header(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?$' + '^\\/catchall-header(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$' ), source: '/catchall-header/:path*', }, @@ -1452,7 +1508,7 @@ const runTests = (isDev = false) => { value: 'header', }, ], - regex: normalizeRegEx('^\\/has-header-1$'), + regex: normalizeRegEx('^\\/has-header-1(?:\\/)?$'), source: '/has-header-1', }, { @@ -1468,7 +1524,7 @@ const runTests = (isDev = false) => { value: 'value', }, ], - regex: normalizeRegEx('^\\/has-header-2$'), + regex: normalizeRegEx('^\\/has-header-2(?:\\/)?$'), source: '/has-header-2', }, { @@ -1485,7 +1541,7 @@ const runTests = (isDev = false) => { value: 'yuuuup', }, ], - regex: normalizeRegEx('^\\/has-header-3$'), + regex: normalizeRegEx('^\\/has-header-3(?:\\/)?$'), source: '/has-header-3', }, { @@ -1501,7 +1557,7 @@ const runTests = (isDev = false) => { value: 'yuuuup', }, ], - regex: normalizeRegEx('^\\/has-header-4$'), + regex: normalizeRegEx('^\\/has-header-4(?:\\/)?$'), source: '/has-header-4', }, ], @@ -1515,13 +1571,13 @@ const runTests = (isDev = false) => { type: 'query', }, ], - regex: normalizeRegEx('^\\/hello$'), + regex: normalizeRegEx('^\\/hello(?:\\/)?$'), source: '/hello', }, { destination: '/blog/:path*', regex: normalizeRegEx( - '^\\/old-blog(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?$' + '^\\/old-blog(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$' ), source: '/old-blog/:path*', }, @@ -1529,147 +1585,151 @@ const runTests = (isDev = false) => { afterFiles: [ { destination: 'http://localhost:12233', - regex: normalizeRegEx('^\\/to-nowhere$'), + regex: normalizeRegEx('^\\/to-nowhere(?:\\/)?$'), source: '/to-nowhere', }, { destination: '/auto-export/hello?rewrite=1', - regex: normalizeRegEx('^\\/rewriting-to-auto-export$'), + regex: normalizeRegEx('^\\/rewriting-to-auto-export(?:\\/)?$'), source: '/rewriting-to-auto-export', }, { destination: '/auto-export/another?rewrite=1', regex: normalizeRegEx( - '^\\/rewriting-to-another-auto-export(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?$' + '^\\/rewriting-to-another-auto-export(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$' ), source: '/rewriting-to-another-auto-export/:path*', }, { destination: '/another/one', - regex: normalizeRegEx('^\\/to-another$'), + regex: normalizeRegEx('^\\/to-another(?:\\/)?$'), source: '/to-another', }, { destination: '/404', - regex: '^\\/nav$', + regex: normalizeRegEx('^\\/nav(?:\\/)?$'), source: '/nav', }, { source: '/hello-world', destination: '/static/hello.txt', - regex: normalizeRegEx('^\\/hello-world$'), + regex: normalizeRegEx('^\\/hello-world(?:\\/)?$'), }, { source: '/', destination: '/another', - regex: normalizeRegEx('^\\/$'), + regex: normalizeRegEx('^\\/(?:\\/)?$'), }, { source: '/another', destination: '/multi-rewrites', - regex: normalizeRegEx('^\\/another$'), + regex: normalizeRegEx('^\\/another(?:\\/)?$'), }, { source: '/first', destination: '/hello', - regex: normalizeRegEx('^\\/first$'), + regex: normalizeRegEx('^\\/first(?:\\/)?$'), }, { source: '/second', destination: '/hello-again', - regex: normalizeRegEx('^\\/second$'), + regex: normalizeRegEx('^\\/second(?:\\/)?$'), }, { destination: '/hello', - regex: normalizeRegEx('^\\/to-hello$'), + regex: normalizeRegEx('^\\/to-hello(?:\\/)?$'), source: '/to-hello', }, { destination: '/blog/post-2', - regex: normalizeRegEx('^\\/blog\\/post-1$'), + regex: normalizeRegEx('^\\/blog\\/post-1(?:\\/)?$'), source: '/blog/post-1', }, { source: '/test/:path', destination: '/:path', - regex: normalizeRegEx('^\\/test(?:\\/([^\\/]+?))$'), + regex: normalizeRegEx('^\\/test(?:\\/([^\\/]+?))(?:\\/)?$'), }, { source: '/test-overwrite/:something/:another', destination: '/params/this-should-be-the-value', regex: normalizeRegEx( - '^\\/test-overwrite(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))$' + '^\\/test-overwrite(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))(?:\\/)?$' ), }, { source: '/params/:something', destination: '/with-params', - regex: normalizeRegEx('^\\/params(?:\\/([^\\/]+?))$'), + regex: normalizeRegEx('^\\/params(?:\\/([^\\/]+?))(?:\\/)?$'), }, { destination: '/with-params?first=:section&second=:name', regex: normalizeRegEx( - '^\\/query-rewrite(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))$' + '^\\/query-rewrite(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))(?:\\/)?$' ), source: '/query-rewrite/:section/:name', }, { destination: '/_next/:path*', regex: normalizeRegEx( - '^\\/hidden\\/_next(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?$' + '^\\/hidden\\/_next(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$' ), source: '/hidden/_next/:path*', }, { destination: `http://localhost:${externalServerPort}/:path*`, regex: normalizeRegEx( - '^\\/proxy-me(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?$' + '^\\/proxy-me(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$' ), source: '/proxy-me/:path*', }, { destination: '/api/hello', - regex: normalizeRegEx('^\\/api-hello$'), + regex: normalizeRegEx('^\\/api-hello(?:\\/)?$'), source: '/api-hello', }, { destination: '/api/hello?name=:first*', - regex: normalizeRegEx('^\\/api-hello-regex(?:\\/(.*))$'), + regex: normalizeRegEx('^\\/api-hello-regex(?:\\/(.*))(?:\\/)?$'), source: '/api-hello-regex/:first(.*)', }, { destination: '/api/hello?hello=:name', - regex: normalizeRegEx('^\\/api-hello-param(?:\\/([^\\/]+?))$'), + regex: normalizeRegEx( + '^\\/api-hello-param(?:\\/([^\\/]+?))(?:\\/)?$' + ), source: '/api-hello-param/:name', }, { destination: '/api/dynamic/:name?hello=:name', - regex: normalizeRegEx('^\\/api-dynamic-param(?:\\/([^\\/]+?))$'), + regex: normalizeRegEx( + '^\\/api-dynamic-param(?:\\/([^\\/]+?))(?:\\/)?$' + ), source: '/api-dynamic-param/:name', }, { destination: '/with-params', - regex: normalizeRegEx('^(?:\\/([^\\/]+?))\\/post-321$'), + regex: normalizeRegEx('^(?:\\/([^\\/]+?))\\/post-321(?:\\/)?$'), source: '/:path/post-321', }, { destination: '/with-params', regex: normalizeRegEx( - '^\\/unnamed-params\\/nested(?:\\/(.*))(?:\\/([^\\/]+?))(?:\\/(.*))$' + '^\\/unnamed-params\\/nested(?:\\/(.*))(?:\\/([^\\/]+?))(?:\\/(.*))(?:\\/)?$' ), source: '/unnamed-params/nested/(.*)/:test/(.*)', }, { destination: '/with-params', regex: normalizeRegEx( - '^\\/catchall-rewrite(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?$' + '^\\/catchall-rewrite(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$' ), source: '/catchall-rewrite/:path*', }, { destination: '/with-params?another=:path*', regex: normalizeRegEx( - '^\\/catchall-query(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?$' + '^\\/catchall-query(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$' ), source: '/catchall-query/:path*', }, @@ -1682,7 +1742,7 @@ const runTests = (isDev = false) => { value: '(?.*)', }, ], - regex: normalizeRegEx('^\\/has-rewrite-1$'), + regex: normalizeRegEx('^\\/has-rewrite-1(?:\\/)?$'), source: '/has-rewrite-1', }, { @@ -1693,7 +1753,7 @@ const runTests = (isDev = false) => { type: 'query', }, ], - regex: normalizeRegEx('^\\/has-rewrite-2$'), + regex: normalizeRegEx('^\\/has-rewrite-2(?:\\/)?$'), source: '/has-rewrite-2', }, { @@ -1705,7 +1765,7 @@ const runTests = (isDev = false) => { value: '(?true)', }, ], - regex: normalizeRegEx('^\\/has-rewrite-3$'), + regex: normalizeRegEx('^\\/has-rewrite-3(?:\\/)?$'), source: '/has-rewrite-3', }, { @@ -1716,7 +1776,7 @@ const runTests = (isDev = false) => { value: 'example.com', }, ], - regex: '^\\/has-rewrite-4$', + regex: normalizeRegEx('^\\/has-rewrite-4(?:\\/)?$'), source: '/has-rewrite-4', }, { @@ -1727,7 +1787,7 @@ const runTests = (isDev = false) => { type: 'query', }, ], - regex: normalizeRegEx('^\\/has-rewrite-5$'), + regex: normalizeRegEx('^\\/has-rewrite-5(?:\\/)?$'), source: '/has-rewrite-5', }, { @@ -1739,7 +1799,7 @@ const runTests = (isDev = false) => { value: 'with-params', }, ], - regex: normalizeRegEx('^\\/has-rewrite-6$'), + regex: normalizeRegEx('^\\/has-rewrite-6(?:\\/)?$'), source: '/has-rewrite-6', }, { @@ -1751,7 +1811,7 @@ const runTests = (isDev = false) => { value: '(?with-params|hello)', }, ], - regex: normalizeRegEx('^\\/has-rewrite-7$'), + regex: normalizeRegEx('^\\/has-rewrite-7(?:\\/)?$'), source: '/has-rewrite-7', }, { @@ -1762,12 +1822,12 @@ const runTests = (isDev = false) => { type: 'query', }, ], - regex: normalizeRegEx('^\\/has-rewrite-8$'), + regex: normalizeRegEx('^\\/has-rewrite-8(?:\\/)?$'), source: '/has-rewrite-8', }, { destination: '/hello', - regex: normalizeRegEx('^\\/blog\\/about$'), + regex: normalizeRegEx('^\\/blog\\/about(?:\\/)?$'), source: '/blog/about', }, ],