Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/next/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -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 =
Expand Down
21 changes: 6 additions & 15 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,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,
Expand Down Expand Up @@ -113,11 +113,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,
Expand Down Expand Up @@ -1090,18 +1086,13 @@ export default abstract class Server<ServerOptions extends Options = Options> {
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
Expand Down
35 changes: 32 additions & 3 deletions packages/next/src/server/server-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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]
Expand All @@ -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,
Expand Down
10 changes: 3 additions & 7 deletions packages/next/src/server/web/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ 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 { getEdgePreviewProps } from './get-edge-preview-props'
import { normalizeNextQueryParam } from '../server-utils'

export class NextRequestHint extends NextRequest {
sourcePage: string
Expand Down Expand Up @@ -104,18 +104,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.
Expand Down
7 changes: 4 additions & 3 deletions packages/next/src/shared/lib/router/utils/route-regex.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return 'intercepted'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => null
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return 'not intercepted'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default function Layout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<>
<div id="children">{children}</div>
<div id="modal">{modal}</div>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Link from 'next/link'

export default function Page() {
return (
<div>
<Link href="/foo/p/1">Foo</Link> <Link href="/foo/p/1">Foo</Link>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Default() {
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default function Layout(props: { children: React.ReactNode }) {
return (
<html>
<body>
<div>{props.children}</div>
</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createNextDescribe } from 'e2e-utils'
import { check } from 'next-test-utils'

createNextDescribe(
'interception-dynamic-segment-middleware',
{
files: __dirname,
},
({ next }) => {
it('should work when interception route is paired with a dynamic segment & middleware', async () => {
const browser = await next.browser('/')

await browser.elementByCss('[href="/foo/p/1"]').click()
await check(() => browser.elementById('modal').text(), /intercepted/)
await browser.refresh()
await check(() => browser.elementById('modal').text(), '')
await check(
() => browser.elementById('children').text(),
/not intercepted/
)
})
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NextResponse, type NextRequest } from 'next/server'

export default async function middleware(request: NextRequest) {
const locale = 'en'
const { pathname } = request.nextUrl
const pathnameHasLocale =
pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
if (pathnameHasLocale) return

request.nextUrl.pathname = `/en${pathname}`
return NextResponse.rewrite(request.nextUrl)
}

export const config = {
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}

module.exports = nextConfig
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,9 @@ exports[`Next Build production mode first time setup with TypeScript 1`] = `
"no-var": [
"error",
],
"no-with": [
"off",
],
"prefer-const": [
"error",
],
Expand Down
Loading