Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minimal mode normalizing #21083

Merged
merged 10 commits into from Jan 16, 2021
185 changes: 101 additions & 84 deletions packages/next/next-server/server/next-server.ts
Expand Up @@ -314,23 +314,116 @@ export default class Server {
const url: any = req.url
parsedUrl = parseUrl(url, true)
}
const { basePath, i18n } = this.nextConfig

// Parse the querystring ourselves if the user doesn't handle querystring parsing
if (typeof parsedUrl.query === 'string') {
parsedUrl.query = parseQs(parsedUrl.query)
}
;(req as any).__NEXT_INIT_QUERY = Object.assign({}, parsedUrl.query)

const { basePath, i18n } = this.nextConfig

if (basePath && req.url?.startsWith(basePath)) {
// store original URL to allow checking if basePath was
// provided or not
;(req as any)._nextHadBasePath = true
req.url = req.url!.replace(basePath, '') || '/'
}

if (i18n && !req.url?.startsWith('/_next')) {
if (
this.minimalMode &&
req.headers['x-matched-path'] &&
typeof req.headers['x-matched-path'] === 'string'
) {
const reqUrlIsDataUrl = req.url?.includes('/_next/data')
const matchedPathIsDataUrl = req.headers['x-matched-path']?.includes(
'/_next/data'
)
const isDataUrl = reqUrlIsDataUrl || matchedPathIsDataUrl

let parsedPath = parseUrl(
isDataUrl ? req.url! : (req.headers['x-matched-path'] as string),
true
)
const { pathname, query } = parsedPath
let matchedPathname = pathname as string

const matchedPathnameNoExt = isDataUrl
? matchedPathname.replace(/\.json$/, '')
: matchedPathname

if (i18n) {
const localePathResult = normalizeLocalePath(
matchedPathname || '/',
i18n.locales
)

if (localePathResult.detectedLocale) {
parsedUrl.query.__nextLocale = localePathResult.detectedLocale
}
}

const pageIsDynamic = isDynamicRoute(matchedPathnameNoExt)
const utils = getUtils({
pageIsDynamic,
page: matchedPathnameNoExt,
i18n: this.nextConfig.i18n,
basePath: this.nextConfig.basePath,
rewrites: this.customRoutes.rewrites,
})

utils.handleRewrites(parsedUrl)

// interpolate dynamic params and normalize URL if needed
if (pageIsDynamic) {
let params: ParsedUrlQuery | false = {}
const paramsResult = utils.normalizeDynamicRouteParams({
...parsedUrl.query,
...query,
})

if (paramsResult.hasValidParams) {
params = paramsResult.params
} else if (req.headers['x-now-route-matches']) {
const opts: Record<string, string> = {}
params = utils.getParamsFromRouteMatches(
req,
opts,
(parsedUrl.query.__nextLocale as string | undefined) || ''
)

if (opts.locale) {
parsedUrl.query.__nextLocale = opts.locale
}
} else {
params = utils.dynamicRouteMatcher!(matchedPathnameNoExt)
}

if (params) {
params = utils.normalizeDynamicRouteParams(params).params

matchedPathname = utils.interpolateDynamicPath(
matchedPathname,
params
)
req.url = utils.interpolateDynamicPath(req.url!, params)
}

if (reqUrlIsDataUrl && matchedPathIsDataUrl) {
req.url = formatUrl({
...parsedPath,
pathname: matchedPathname,
})
}
Object.assign(parsedUrl.query, params)
utils.normalizeVercelUrl(req, true)
}

parsedUrl.pathname = `${basePath || ''}${
matchedPathname === '/' && basePath ? '' : matchedPathname
}`
}

if (i18n) {
// get pathname from URL with basePath stripped for locale detection
let { pathname, ...parsed } = parseUrl(req.url || '/')
pathname = pathname || '/'
Expand Down Expand Up @@ -462,88 +555,12 @@ export default class Server {
parsedUrl.query.__nextDefaultLocale =
detectedDomain?.defaultLocale || i18n.defaultLocale

parsedUrl.query.__nextLocale =
localePathResult.detectedLocale ||
detectedDomain?.defaultLocale ||
defaultLocale
}

if (
this.minimalMode &&
req.headers['x-matched-path'] &&
typeof req.headers['x-matched-path'] === 'string'
) {
const reqUrlIsDataUrl = req.url?.includes('/_next/data')
const matchedPathIsDataUrl = req.headers['x-matched-path']?.includes(
'/_next/data'
)
const isDataUrl = reqUrlIsDataUrl || matchedPathIsDataUrl

let parsedPath = parseUrl(
isDataUrl ? req.url! : (req.headers['x-matched-path'] as string),
true
)
const { pathname, query } = parsedPath
let matchedPathname = pathname as string

const matchedPathnameNoExt = isDataUrl
? matchedPathname.replace(/\.json$/, '')
: matchedPathname

// interpolate dynamic params and normalize URL if needed
if (isDynamicRoute(matchedPathnameNoExt)) {
const utils = getUtils({
pageIsDynamic: true,
page: matchedPathnameNoExt,
i18n: this.nextConfig.i18n,
basePath: this.nextConfig.basePath,
rewrites: this.customRoutes.rewrites,
})

let params: ParsedUrlQuery | false = {}
const paramsResult = utils.normalizeDynamicRouteParams({
...parsedUrl.query,
...query,
})

if (paramsResult.hasValidParams) {
params = paramsResult.params
} else if (req.headers['x-now-route-matches']) {
const opts: Record<string, string> = {}
params = utils.getParamsFromRouteMatches(
req,
opts,
(parsedUrl.query.__nextLocale as string | undefined) || ''
)

if (opts.locale) {
parsedUrl.query.__nextLocale = opts.locale
}
} else {
params = utils.dynamicRouteMatcher!(matchedPathname)
}

if (params) {
matchedPathname = utils.interpolateDynamicPath(
matchedPathname,
params
)

req.url = utils.interpolateDynamicPath(req.url!, params)
}

if (reqUrlIsDataUrl && matchedPathIsDataUrl) {
req.url = formatUrl({
...parsedPath,
pathname: matchedPathname,
})
}
Object.assign(parsedUrl.query, params)
utils.normalizeVercelUrl(req, true)
if (!this.minimalMode || !parsedUrl.query.__nextLocale) {
parsedUrl.query.__nextLocale =
localePathResult.detectedLocale ||
detectedDomain?.defaultLocale ||
defaultLocale
}
parsedUrl.pathname = `${basePath || ''}${
parsedUrl.query.__nextLocale || ''
}${matchedPathname}`
}

res.statusCode = 200
Expand Down
10 changes: 10 additions & 0 deletions test/integration/required-server-files/next.config.js
@@ -0,0 +1,10 @@
module.exports = {
rewrites() {
return [
{
source: '/some-catch-all/:path*',
destination: '/',
},
]
},
}
@@ -0,0 +1,29 @@
import { useRouter } from 'next/router'

export const getStaticProps = ({ params }) => {
return {
props: {
hello: 'world',
params: params || null,
random: Math.random(),
},
}
}

export const getStaticPaths = () => {
return {
paths: ['/catch-all/hello'],
fallback: true,
}
}

export default function Page(props) {
const router = useRouter()
return (
<>
<p id="catch-all">optional catch-all page</p>
<p id="router">{JSON.stringify(router)}</p>
<p id="props">{JSON.stringify(props)}</p>
</>
)
}
129 changes: 129 additions & 0 deletions test/integration/required-server-files/test/index.test.js
Expand Up @@ -273,6 +273,116 @@ describe('Required Server Files', () => {
expect(data2.hello).toBe('world')
})

it('should render fallback optional catch-all route correctly with x-matched-path and routes-matches', async () => {
const html = await renderViaHTTP(
appPort,
'/catch-all/[[...rest]]',
undefined,
{
headers: {
'x-matched-path': '/catch-all/[[...rest]]',
'x-now-route-matches': '',
},
}
)
const $ = cheerio.load(html)
const data = JSON.parse($('#props').text())

expect($('#catch-all').text()).toBe('optional catch-all page')
expect(data.params).toEqual({})
expect(data.hello).toBe('world')

const html2 = await renderViaHTTP(
appPort,
'/catch-all/[[...rest]]',
undefined,
{
headers: {
'x-matched-path': '/catch-all/[[...rest]]',
'x-now-route-matches': '1=hello&catchAll=hello',
},
}
)
const $2 = cheerio.load(html2)
const data2 = JSON.parse($2('#props').text())

expect($2('#catch-all').text()).toBe('optional catch-all page')
expect(data2.params).toEqual({ rest: ['hello'] })
expect(isNaN(data2.random)).toBe(false)
expect(data2.random).not.toBe(data.random)

const html3 = await renderViaHTTP(
appPort,
'/catch-all/[[..rest]]',
undefined,
{
headers: {
'x-matched-path': '/catch-all/[[...rest]]',
'x-now-route-matches': '1=hello/world&catchAll=hello/world',
},
}
)
const $3 = cheerio.load(html3)
const data3 = JSON.parse($3('#props').text())

expect($3('#catch-all').text()).toBe('optional catch-all page')
expect(data3.params).toEqual({ rest: ['hello', 'world'] })
expect(isNaN(data3.random)).toBe(false)
expect(data3.random).not.toBe(data.random)
})

it('should return data correctly with x-matched-path for optional catch-all route', async () => {
const res = await fetchViaHTTP(
appPort,
`/_next/data/${buildId}/catch-all.json`,
undefined,
{
headers: {
'x-matched-path': '/catch-all/[[...rest]]',
},
}
)

const { pageProps: data } = await res.json()

expect(data.params).toEqual({})
expect(data.hello).toBe('world')

const res2 = await fetchViaHTTP(
appPort,
`/_next/data/${buildId}/catch-all/[[...rest]].json`,
undefined,
{
headers: {
'x-matched-path': `/_next/data/${buildId}/catch-all/[[...rest]].json`,
'x-now-route-matches': '1=hello&rest=hello',
},
}
)

const { pageProps: data2 } = await res2.json()

expect(data2.params).toEqual({ rest: ['hello'] })
expect(data2.hello).toBe('world')

const res3 = await fetchViaHTTP(
appPort,
`/_next/data/${buildId}/catch-all/[[...rest]].json`,
undefined,
{
headers: {
'x-matched-path': `/_next/data/${buildId}/catch-all/[[...rest]].json`,
'x-now-route-matches': '1=hello/world&rest=hello/world',
},
}
)

const { pageProps: data3 } = await res3.json()

expect(data3.params).toEqual({ rest: ['hello', 'world'] })
expect(data3.hello).toBe('world')
})

it('should not apply trailingSlash redirect', async () => {
for (const path of [
'/',
Expand All @@ -290,4 +400,23 @@ describe('Required Server Files', () => {
expect(res.status).toBe(200)
}
})

it('should normalize catch-all rewrite query values correctly', async () => {
const html = await renderViaHTTP(
appPort,
'/some-catch-all/hello/world',
{
path: 'hello/world',
},
{
headers: {
'x-matched-path': '/',
},
}
)
const $ = cheerio.load(html)
expect(JSON.parse($('#router').text()).query).toEqual({
path: ['hello', 'world'],
})
})
})