Skip to content

Commit

Permalink
fix: properly call normalizeDynamicRouteParams in `NextWebServer.ha…
Browse files Browse the repository at this point in the history
…ndleCatchAllRenderRequest` (#58949)

fixes #53682

This follows the same implementation as
`Server.handleCatchallRenderRequest` (base-server).
  • Loading branch information
smaeda-ks authored Dec 5, 2023
1 parent 40cb6f4 commit faa4421
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 83 deletions.
173 changes: 92 additions & 81 deletions packages/next/src/server/server-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,88 @@ export function interpolateDynamicPath(
return pathname
}

export function normalizeDynamicRouteParams(
params: ParsedUrlQuery,
ignoreOptional?: boolean,
defaultRouteRegex?: ReturnType<typeof getNamedRouteRegex> | undefined,
defaultRouteMatches?: ParsedUrlQuery | undefined
) {
let hasValidParams = true
if (!defaultRouteRegex) return { params, hasValidParams: false }

params = Object.keys(defaultRouteRegex.groups).reduce((prev, key) => {
let value: string | string[] | undefined = params[key]

if (typeof value === 'string') {
value = normalizeRscURL(value)
}
if (Array.isArray(value)) {
value = value.map((val) => {
if (typeof val === 'string') {
val = normalizeRscURL(val)
}
return val
})
}

// if the value matches the default value we can't rely
// on the parsed params, this is used to signal if we need
// to parse x-now-route-matches or not
const defaultValue = defaultRouteMatches![key]
const isOptional = defaultRouteRegex!.groups[key].optional

const isDefaultValue = Array.isArray(defaultValue)
? defaultValue.some((defaultVal) => {
return Array.isArray(value)
? value.some((val) => val.includes(defaultVal))
: value?.includes(defaultVal)
})
: value?.includes(defaultValue as string)

if (
isDefaultValue ||
(typeof value === 'undefined' && !(isOptional && ignoreOptional))
) {
hasValidParams = false
}

// non-provided optional values should be undefined so normalize
// them to undefined
if (
isOptional &&
(!value ||
(Array.isArray(value) &&
value.length === 1 &&
// fallback optional catch-all SSG pages have
// [[...paramName]] for the root path on Vercel
(value[0] === 'index' || value[0] === `[[...${key}]]`)))
) {
value = undefined
delete params[key]
}

// query values from the proxy aren't already split into arrays
// so make sure to normalize catch-all values
if (
value &&
typeof value === 'string' &&
defaultRouteRegex!.groups[key].repeat
) {
value = value.split('/')
}

if (value) {
prev[key] = value
}
return prev
}, {} as ParsedUrlQuery)

return {
params,
hasValidParams,
}
}

export function getUtils({
page,
i18n,
Expand Down Expand Up @@ -326,93 +408,22 @@ export function getUtils({
)(req.headers['x-now-route-matches'] as string) as ParsedUrlQuery
}

function normalizeDynamicRouteParams(
params: ParsedUrlQuery,
ignoreOptional?: boolean
) {
let hasValidParams = true
if (!defaultRouteRegex) return { params, hasValidParams: false }

params = Object.keys(defaultRouteRegex.groups).reduce((prev, key) => {
let value: string | string[] | undefined = params[key]

if (typeof value === 'string') {
value = normalizeRscURL(value)
}
if (Array.isArray(value)) {
value = value.map((val) => {
if (typeof val === 'string') {
val = normalizeRscURL(val)
}
return val
})
}

// if the value matches the default value we can't rely
// on the parsed params, this is used to signal if we need
// to parse x-now-route-matches or not
const defaultValue = defaultRouteMatches![key]
const isOptional = defaultRouteRegex!.groups[key].optional

const isDefaultValue = Array.isArray(defaultValue)
? defaultValue.some((defaultVal) => {
return Array.isArray(value)
? value.some((val) => val.includes(defaultVal))
: value?.includes(defaultVal)
})
: value?.includes(defaultValue as string)

if (
isDefaultValue ||
(typeof value === 'undefined' && !(isOptional && ignoreOptional))
) {
hasValidParams = false
}

// non-provided optional values should be undefined so normalize
// them to undefined
if (
isOptional &&
(!value ||
(Array.isArray(value) &&
value.length === 1 &&
// fallback optional catch-all SSG pages have
// [[...paramName]] for the root path on Vercel
(value[0] === 'index' || value[0] === `[[...${key}]]`)))
) {
value = undefined
delete params[key]
}

// query values from the proxy aren't already split into arrays
// so make sure to normalize catch-all values
if (
value &&
typeof value === 'string' &&
defaultRouteRegex!.groups[key].repeat
) {
value = value.split('/')
}

if (value) {
prev[key] = value
}
return prev
}, {} as ParsedUrlQuery)

return {
params,
hasValidParams,
}
}

return {
handleRewrites,
defaultRouteRegex,
dynamicRouteMatcher,
defaultRouteMatches,
getParamsFromRouteMatches,
normalizeDynamicRouteParams,
normalizeDynamicRouteParams: (
params: ParsedUrlQuery,
ignoreOptional?: boolean
) =>
normalizeDynamicRouteParams(
params,
ignoreOptional,
defaultRouteRegex,
defaultRouteMatches
),
normalizeVercelUrl: (
req: BaseNextRequest,
trustQuery: boolean,
Expand Down
27 changes: 25 additions & 2 deletions packages/next/src/server/web-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ import WebResponseCache from './response-cache/web'
import { isAPIRoute } from '../lib/is-api-route'
import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash'
import { isDynamicRoute } from '../shared/lib/router/utils'
import { interpolateDynamicPath, normalizeVercelUrl } from './server-utils'
import {
interpolateDynamicPath,
normalizeVercelUrl,
normalizeDynamicRouteParams,
} from './server-utils'
import { getNamedRouteRegex } from '../shared/lib/router/utils/route-regex'
import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher'
import { IncrementalCache } from './lib/incremental-cache'

interface WebServerOptions extends Options {
Expand Down Expand Up @@ -158,7 +163,25 @@ export default class NextWebServer extends BaseServer<WebServerOptions> {

if (isDynamicRoute(pathname)) {
const routeRegex = getNamedRouteRegex(pathname, false)
pathname = interpolateDynamicPath(pathname, query, routeRegex)
const dynamicRouteMatcher = getRouteMatcher(routeRegex)
const defaultRouteMatches = dynamicRouteMatcher(
pathname
) as NextParsedUrlQuery
const paramsResult = normalizeDynamicRouteParams(
query,
false,
routeRegex,
defaultRouteMatches
)
const normalizedParams = paramsResult.hasValidParams
? paramsResult.params
: query

pathname = interpolateDynamicPath(
pathname,
normalizedParams,
routeRegex
)
normalizeVercelUrl(
req,
true,
Expand Down
9 changes: 9 additions & 0 deletions test/e2e/app-dir/app/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,15 @@ createNextDescribe(
})
})

it('should return normalized dynamic route params for catch-all edge page', async () => {
const html = await next.render('/catch-all-edge/a/b/c')
const $ = cheerio.load(html)

expect(JSON.parse($('#params').text())).toEqual({
slug: ['a', 'b', 'c'],
})
})

it('should have correct searchParams and params (server)', async () => {
const html = await next.render('/dynamic/category-1/id-2?query1=value2')
const $ = cheerio.load(html)
Expand Down

0 comments on commit faa4421

Please sign in to comment.