diff --git a/packages/next/src/lib/constants.ts b/packages/next/src/lib/constants.ts index 11a1d792ec2435..f8ebd11e33c137 100644 --- a/packages/next/src/lib/constants.ts +++ b/packages/next/src/lib/constants.ts @@ -1,6 +1,7 @@ import type { ServerRuntime } from '../types' export const NEXT_QUERY_PARAM_PREFIX = 'nxtP' +export const NEXT_INTERCEPTION_MARKER_PREFIX = 'nxtI' export const PRERENDER_REVALIDATE_HEADER = 'x-prerender-revalidate' export const PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER = diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 0854bac176c7d8..16c8abf1058dcf 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -70,7 +70,7 @@ import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing- import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path' import * as Log from '../build/output/log' import escapePathDelimiters from '../shared/lib/router/utils/escape-path-delimiters' -import { getUtils } from './server-utils' +import { getUtils, normalizeNextQueryParam } from './server-utils' import isError, { getProperError } from '../lib/is-error' import { addRequestMeta, @@ -111,11 +111,7 @@ import { fromNodeOutgoingHttpHeaders, toNodeOutgoingHttpHeaders, } from './web/utils' -import { - CACHE_ONE_YEAR, - NEXT_CACHE_TAGS_HEADER, - NEXT_QUERY_PARAM_PREFIX, -} from '../lib/constants' +import { CACHE_ONE_YEAR, NEXT_CACHE_TAGS_HEADER } from '../lib/constants' import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path' import { NextRequestAdapter, @@ -1096,18 +1092,13 @@ export default abstract class Server< for (const key of Object.keys(parsedUrl.query)) { const value = parsedUrl.query[key] - if ( - key !== NEXT_QUERY_PARAM_PREFIX && - key.startsWith(NEXT_QUERY_PARAM_PREFIX) - ) { - const normalizedKey = key.substring( - NEXT_QUERY_PARAM_PREFIX.length - ) - parsedUrl.query[normalizedKey] = value + normalizeNextQueryParam(key, (normalizedKey) => { + if (!parsedUrl) return // typeguard + parsedUrl.query[normalizedKey] = value routeParamKeys.add(normalizedKey) delete parsedUrl.query[key] - } + }) } // interpolate dynamic params and normalize URL if needed diff --git a/packages/next/src/server/server-utils.ts b/packages/next/src/server/server-utils.ts index 3053ec32d803b6..e5f0dbd485a015 100644 --- a/packages/next/src/server/server-utils.ts +++ b/packages/next/src/server/server-utils.ts @@ -16,7 +16,10 @@ import { } from '../shared/lib/router/utils/prepare-destination' import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash' import { normalizeRscURL } from '../shared/lib/router/utils/app-paths' -import { NEXT_QUERY_PARAM_PREFIX } from '../lib/constants' +import { + NEXT_INTERCEPTION_MARKER_PREFIX, + NEXT_QUERY_PARAM_PREFIX, +} from '../lib/constants' export function normalizeVercelUrl( req: BaseNextRequest, @@ -32,9 +35,17 @@ export function normalizeVercelUrl( delete (_parsedUrl as any).search for (const key of Object.keys(_parsedUrl.query)) { + const isNextQueryPrefix = + key !== NEXT_QUERY_PARAM_PREFIX && + key.startsWith(NEXT_QUERY_PARAM_PREFIX) + + const isNextInterceptionMarkerPrefix = + key !== NEXT_INTERCEPTION_MARKER_PREFIX && + key.startsWith(NEXT_INTERCEPTION_MARKER_PREFIX) + if ( - (key !== NEXT_QUERY_PARAM_PREFIX && - key.startsWith(NEXT_QUERY_PARAM_PREFIX)) || + isNextQueryPrefix || + isNextInterceptionMarkerPrefix || (paramKeys || Object.keys(defaultRouteRegex.groups)).includes(key) ) { delete _parsedUrl.query[key] @@ -44,6 +55,24 @@ export function normalizeVercelUrl( } } +/** + * Normalizes `nxtP` and `nxtI` query param values to remove the prefix. + * This function does not mutate the input key; it calls the provided function + * with the normalized key. + */ +export function normalizeNextQueryParam( + key: string, + onKeyNormalized: (normalizedKey: string) => void +) { + const prefixes = [NEXT_QUERY_PARAM_PREFIX, NEXT_INTERCEPTION_MARKER_PREFIX] + for (const prefix of prefixes) { + if (key !== prefix && key.startsWith(prefix)) { + const normalizedKey = key.substring(prefix.length) + onKeyNormalized(normalizedKey) + } + } +} + export function interpolateDynamicPath( pathname: string, params: ParsedUrlQuery, diff --git a/packages/next/src/server/web/adapter.ts b/packages/next/src/server/web/adapter.ts index 1a54ae7c5f6496..1b628dfcea266f 100644 --- a/packages/next/src/server/web/adapter.ts +++ b/packages/next/src/server/web/adapter.ts @@ -12,13 +12,13 @@ import { NextURL } from './next-url' import { stripInternalSearchParams } from '../internal-utils' import { normalizeRscURL } from '../../shared/lib/router/utils/app-paths' import { FLIGHT_PARAMETERS } from '../../client/components/app-router-headers' -import { NEXT_QUERY_PARAM_PREFIX } from '../../lib/constants' import { ensureInstrumentationRegistered } from './globals' import { RequestAsyncStorageWrapper } from '../async-storage/request-async-storage-wrapper' import { requestAsyncStorage } from '../../client/components/request-async-storage.external' import { getTracer } from '../lib/trace/tracer' import type { TextMapGetter } from 'next/dist/compiled/@opentelemetry/api' import { MiddlewareSpan } from '../lib/trace/constants' +import { normalizeNextQueryParam } from '../server-utils' export class NextRequestHint extends NextRequest { sourcePage: string @@ -108,18 +108,14 @@ export async function adapter( for (const key of keys) { const value = requestUrl.searchParams.getAll(key) - if ( - key !== NEXT_QUERY_PARAM_PREFIX && - key.startsWith(NEXT_QUERY_PARAM_PREFIX) - ) { - const normalizedKey = key.substring(NEXT_QUERY_PARAM_PREFIX.length) + normalizeNextQueryParam(key, (normalizedKey) => { requestUrl.searchParams.delete(normalizedKey) for (const val of value) { requestUrl.searchParams.append(normalizedKey, val) } requestUrl.searchParams.delete(key) - } + }) } // Ensure users only see page requests, never data requests. diff --git a/packages/next/src/shared/lib/router/utils/route-regex.ts b/packages/next/src/shared/lib/router/utils/route-regex.ts index 5ec7668fd90f60..f6bff51cbfd03a 100644 --- a/packages/next/src/shared/lib/router/utils/route-regex.ts +++ b/packages/next/src/shared/lib/router/utils/route-regex.ts @@ -1,10 +1,11 @@ +import { + NEXT_INTERCEPTION_MARKER_PREFIX, + NEXT_QUERY_PARAM_PREFIX, +} from '../../../../lib/constants' import { INTERCEPTION_ROUTE_MARKERS } from '../../../../server/future/helpers/interception-routes' import { escapeStringRegexp } from '../../escape-regexp' import { removeTrailingSlash } from './remove-trailing-slash' -const NEXT_QUERY_PARAM_PREFIX = 'nxtP' -const NEXT_INTERCEPTION_MARKER_PREFIX = 'nxtI' - export interface Group { pos: number repeat: boolean diff --git a/test/e2e/app-dir/interception-dynamic-segment-middleware/app/[locale]/@modal/(.)[username]/p/[id]/page.tsx b/test/e2e/app-dir/interception-dynamic-segment-middleware/app/[locale]/@modal/(.)[username]/p/[id]/page.tsx new file mode 100644 index 00000000000000..7c48f6584136b4 --- /dev/null +++ b/test/e2e/app-dir/interception-dynamic-segment-middleware/app/[locale]/@modal/(.)[username]/p/[id]/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return 'intercepted' +} diff --git a/test/e2e/app-dir/interception-dynamic-segment-middleware/app/[locale]/@modal/default.tsx b/test/e2e/app-dir/interception-dynamic-segment-middleware/app/[locale]/@modal/default.tsx new file mode 100644 index 00000000000000..ad4e17b5767f9d --- /dev/null +++ b/test/e2e/app-dir/interception-dynamic-segment-middleware/app/[locale]/@modal/default.tsx @@ -0,0 +1 @@ +export default () => null diff --git a/test/e2e/app-dir/interception-dynamic-segment-middleware/app/[locale]/[username]/p/[id]/page.tsx b/test/e2e/app-dir/interception-dynamic-segment-middleware/app/[locale]/[username]/p/[id]/page.tsx new file mode 100644 index 00000000000000..cab6f9416b983a --- /dev/null +++ b/test/e2e/app-dir/interception-dynamic-segment-middleware/app/[locale]/[username]/p/[id]/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return 'not intercepted' +} diff --git a/test/e2e/app-dir/interception-dynamic-segment-middleware/app/[locale]/layout.tsx b/test/e2e/app-dir/interception-dynamic-segment-middleware/app/[locale]/layout.tsx new file mode 100644 index 00000000000000..c2f731715ce1a9 --- /dev/null +++ b/test/e2e/app-dir/interception-dynamic-segment-middleware/app/[locale]/layout.tsx @@ -0,0 +1,14 @@ +export default function Layout({ + children, + modal, +}: { + children: React.ReactNode + modal: React.ReactNode +}) { + return ( + <> +