Skip to content

Commit 649ba86

Browse files
authored
backport: fix dynamic route interception not working when deployed with middleware (#77794)
Backports: - #64923
1 parent 10a042c commit 649ba86

File tree

16 files changed

+136
-28
lines changed

16 files changed

+136
-28
lines changed

packages/next/src/lib/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ServerRuntime } from '../../types'
22

33
export const NEXT_QUERY_PARAM_PREFIX = 'nxtP'
4+
export const NEXT_INTERCEPTION_MARKER_PREFIX = 'nxtI'
45

56
export const PRERENDER_REVALIDATE_HEADER = 'x-prerender-revalidate'
67
export const PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER =

packages/next/src/server/base-server.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-
7272
import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path'
7373
import * as Log from '../build/output/log'
7474
import escapePathDelimiters from '../shared/lib/router/utils/escape-path-delimiters'
75-
import { getUtils } from './server-utils'
75+
import { getUtils, normalizeNextQueryParam } from './server-utils'
7676
import isError, { getProperError } from '../lib/is-error'
7777
import {
7878
addRequestMeta,
@@ -113,11 +113,7 @@ import {
113113
fromNodeOutgoingHttpHeaders,
114114
toNodeOutgoingHttpHeaders,
115115
} from './web/utils'
116-
import {
117-
CACHE_ONE_YEAR,
118-
NEXT_CACHE_TAGS_HEADER,
119-
NEXT_QUERY_PARAM_PREFIX,
120-
} from '../lib/constants'
116+
import { CACHE_ONE_YEAR, NEXT_CACHE_TAGS_HEADER } from '../lib/constants'
121117
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
122118
import {
123119
NextRequestAdapter,
@@ -1090,18 +1086,13 @@ export default abstract class Server<ServerOptions extends Options = Options> {
10901086
for (const key of Object.keys(parsedUrl.query)) {
10911087
const value = parsedUrl.query[key]
10921088

1093-
if (
1094-
key !== NEXT_QUERY_PARAM_PREFIX &&
1095-
key.startsWith(NEXT_QUERY_PARAM_PREFIX)
1096-
) {
1097-
const normalizedKey = key.substring(
1098-
NEXT_QUERY_PARAM_PREFIX.length
1099-
)
1100-
parsedUrl.query[normalizedKey] = value
1089+
normalizeNextQueryParam(key, (normalizedKey) => {
1090+
if (!parsedUrl) return // typeguard
11011091

1092+
parsedUrl.query[normalizedKey] = value
11021093
routeParamKeys.add(normalizedKey)
11031094
delete parsedUrl.query[key]
1104-
}
1095+
})
11051096
}
11061097

11071098
// interpolate dynamic params and normalize URL if needed

packages/next/src/server/server-utils.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import {
1616
} from '../shared/lib/router/utils/prepare-destination'
1717
import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash'
1818
import { normalizeRscURL } from '../shared/lib/router/utils/app-paths'
19-
import { NEXT_QUERY_PARAM_PREFIX } from '../lib/constants'
19+
import {
20+
NEXT_INTERCEPTION_MARKER_PREFIX,
21+
NEXT_QUERY_PARAM_PREFIX,
22+
} from '../lib/constants'
2023

2124
export function normalizeVercelUrl(
2225
req: BaseNextRequest,
@@ -32,9 +35,17 @@ export function normalizeVercelUrl(
3235
delete (_parsedUrl as any).search
3336

3437
for (const key of Object.keys(_parsedUrl.query)) {
38+
const isNextQueryPrefix =
39+
key !== NEXT_QUERY_PARAM_PREFIX &&
40+
key.startsWith(NEXT_QUERY_PARAM_PREFIX)
41+
42+
const isNextInterceptionMarkerPrefix =
43+
key !== NEXT_INTERCEPTION_MARKER_PREFIX &&
44+
key.startsWith(NEXT_INTERCEPTION_MARKER_PREFIX)
45+
3546
if (
36-
(key !== NEXT_QUERY_PARAM_PREFIX &&
37-
key.startsWith(NEXT_QUERY_PARAM_PREFIX)) ||
47+
isNextQueryPrefix ||
48+
isNextInterceptionMarkerPrefix ||
3849
(paramKeys || Object.keys(defaultRouteRegex.groups)).includes(key)
3950
) {
4051
delete _parsedUrl.query[key]
@@ -44,6 +55,24 @@ export function normalizeVercelUrl(
4455
}
4556
}
4657

58+
/**
59+
* Normalizes `nxtP` and `nxtI` query param values to remove the prefix.
60+
* This function does not mutate the input key; it calls the provided function
61+
* with the normalized key.
62+
*/
63+
export function normalizeNextQueryParam(
64+
key: string,
65+
onKeyNormalized: (normalizedKey: string) => void
66+
) {
67+
const prefixes = [NEXT_QUERY_PARAM_PREFIX, NEXT_INTERCEPTION_MARKER_PREFIX]
68+
for (const prefix of prefixes) {
69+
if (key !== prefix && key.startsWith(prefix)) {
70+
const normalizedKey = key.substring(prefix.length)
71+
onKeyNormalized(normalizedKey)
72+
}
73+
}
74+
}
75+
4776
export function interpolateDynamicPath(
4877
pathname: string,
4978
params: ParsedUrlQuery,

packages/next/src/server/web/adapter.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ import { NextURL } from './next-url'
1111
import { stripInternalSearchParams } from '../internal-utils'
1212
import { normalizeRscURL } from '../../shared/lib/router/utils/app-paths'
1313
import { FLIGHT_PARAMETERS } from '../../client/components/app-router-headers'
14-
import { NEXT_QUERY_PARAM_PREFIX } from '../../lib/constants'
1514
import { ensureInstrumentationRegistered } from './globals'
1615
import { RequestAsyncStorageWrapper } from '../async-storage/request-async-storage-wrapper'
1716
import { requestAsyncStorage } from '../../client/components/request-async-storage.external'
1817
import { getTracer } from '../lib/trace/tracer'
1918
import type { TextMapGetter } from 'next/dist/compiled/@opentelemetry/api'
2019
import { MiddlewareSpan } from '../lib/trace/constants'
2120
import { getEdgePreviewProps } from './get-edge-preview-props'
21+
import { normalizeNextQueryParam } from '../server-utils'
2222

2323
export class NextRequestHint extends NextRequest {
2424
sourcePage: string
@@ -104,18 +104,14 @@ export async function adapter(
104104
for (const key of keys) {
105105
const value = requestUrl.searchParams.getAll(key)
106106

107-
if (
108-
key !== NEXT_QUERY_PARAM_PREFIX &&
109-
key.startsWith(NEXT_QUERY_PARAM_PREFIX)
110-
) {
111-
const normalizedKey = key.substring(NEXT_QUERY_PARAM_PREFIX.length)
107+
normalizeNextQueryParam(key, (normalizedKey) => {
112108
requestUrl.searchParams.delete(normalizedKey)
113109

114110
for (const val of value) {
115111
requestUrl.searchParams.append(normalizedKey, val)
116112
}
117113
requestUrl.searchParams.delete(key)
118-
}
114+
})
119115
}
120116

121117
// Ensure users only see page requests, never data requests.

packages/next/src/shared/lib/router/utils/route-regex.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import {
2+
NEXT_INTERCEPTION_MARKER_PREFIX,
3+
NEXT_QUERY_PARAM_PREFIX,
4+
} from '../../../../lib/constants'
15
import { INTERCEPTION_ROUTE_MARKERS } from '../../../../server/future/helpers/interception-routes'
26
import { escapeStringRegexp } from '../../escape-regexp'
37
import { removeTrailingSlash } from './remove-trailing-slash'
48

5-
const NEXT_QUERY_PARAM_PREFIX = 'nxtP'
6-
const NEXT_INTERCEPTION_MARKER_PREFIX = 'nxtI'
7-
89
export interface Group {
910
pos: number
1011
repeat: boolean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return 'intercepted'
3+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default () => null
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return 'not intercepted'
3+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export default function Layout({
2+
children,
3+
modal,
4+
}: {
5+
children: React.ReactNode
6+
modal: React.ReactNode
7+
}) {
8+
return (
9+
<>
10+
<div id="children">{children}</div>
11+
<div id="modal">{modal}</div>
12+
</>
13+
)
14+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Link from 'next/link'
2+
3+
export default function Page() {
4+
return (
5+
<div>
6+
<Link href="/foo/p/1">Foo</Link> <Link href="/foo/p/1">Foo</Link>
7+
</div>
8+
)
9+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Default() {
2+
return null
3+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export default function Layout(props: { children: React.ReactNode }) {
2+
return (
3+
<html>
4+
<body>
5+
<div>{props.children}</div>
6+
</body>
7+
</html>
8+
)
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { createNextDescribe } from 'e2e-utils'
2+
import { check } from 'next-test-utils'
3+
4+
createNextDescribe(
5+
'interception-dynamic-segment-middleware',
6+
{
7+
files: __dirname,
8+
},
9+
({ next }) => {
10+
it('should work when interception route is paired with a dynamic segment & middleware', async () => {
11+
const browser = await next.browser('/')
12+
13+
await browser.elementByCss('[href="/foo/p/1"]').click()
14+
await check(() => browser.elementById('modal').text(), /intercepted/)
15+
await browser.refresh()
16+
await check(() => browser.elementById('modal').text(), '')
17+
await check(
18+
() => browser.elementById('children').text(),
19+
/not intercepted/
20+
)
21+
})
22+
}
23+
)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { NextResponse, type NextRequest } from 'next/server'
2+
3+
export default async function middleware(request: NextRequest) {
4+
const locale = 'en'
5+
const { pathname } = request.nextUrl
6+
const pathnameHasLocale =
7+
pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
8+
if (pathnameHasLocale) return
9+
10+
request.nextUrl.pathname = `/en${pathname}`
11+
return NextResponse.rewrite(request.nextUrl)
12+
}
13+
14+
export const config = {
15+
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'],
16+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* @type {import('next').NextConfig}
3+
*/
4+
const nextConfig = {}
5+
6+
module.exports = nextConfig

test/production/eslint/test/__snapshots__/next-build-and-lint.test.ts.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,9 @@ exports[`Next Build production mode first time setup with TypeScript 1`] = `
451451
"no-var": [
452452
"error",
453453
],
454+
"no-with": [
455+
"off",
456+
],
454457
"prefer-const": [
455458
"error",
456459
],

0 commit comments

Comments
 (0)