Skip to content

Commit

Permalink
Update to process redirects/rewrites for _next/data with middleware (#…
Browse files Browse the repository at this point in the history
…37574)

* Update to process redirects/rewrites for _next/data

* correct matched-path resolving with middleware

* Add next-data header

* migrate middleware tests

* lint-fix

* update error case

* update test case

* Handle additional resolving cases and add more tests

* update test from merge

* fix test

* rm .only

* apply changes from review

* ensure _next/data resolving does not apply without middleware
  • Loading branch information
ijjk authored Jun 10, 2022
1 parent 78cbfa0 commit 607ff2b
Show file tree
Hide file tree
Showing 65 changed files with 1,310 additions and 1,333 deletions.
5 changes: 4 additions & 1 deletion packages/next/client/page-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export default class PageLoader {
asPath: string
href: string
locale?: string | false
skipInterpolation?: boolean
}): string {
const { asPath, href, locale } = params
const { pathname: hrefPathname, query, search } = parseRelativeUrl(href)
Expand All @@ -138,7 +139,9 @@ export default class PageLoader {
}

return getHrefForSlug(
isDynamicRoute(route)
params.skipInterpolation
? asPathname
: isDynamicRoute(route)
? interpolateAs(hrefPathname, asPathname, query).result
: route
)
Expand Down
178 changes: 86 additions & 92 deletions packages/next/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,71 +439,60 @@ export default abstract class Server<ServerOptions extends Options = Options> {

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

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

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

let srcPathname = isDataUrl
? this.stripNextDataPath(
parsedMatchedPath.pathname?.replace(/\.json$/, '') ||
matchedPathnameNoExt
) || '/'
: matchedPathnameNoExt

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

if (localePathResult.detectedLocale) {
parsedUrl.query.__nextLocale = localePathResult.detectedLocale
try {
// x-matched-path is the source of truth, it tells what page
// should be rendered because we don't process rewrites in minimalMode
let matchedPath = new URL(
req.headers['x-matched-path'],
'http://localhost'
).pathname

let urlPathname = new URL(req.url, 'http://localhost').pathname

// For ISR the URL is normalized to the prerenderPath so if
// it's a data request the URL path will be the data URL,
// basePath is already stripped by this point
if (urlPathname.startsWith(`/_next/data/`)) {
parsedUrl.query.__nextDataReq = '1'
}
}
matchedPath = this.stripNextDataPath(matchedPath, false)

if (isDataUrl) {
matchedPathname = denormalizePagePath(matchedPathname)
matchedPathnameNoExt = denormalizePagePath(matchedPathnameNoExt)
srcPathname = denormalizePagePath(srcPathname)
}
if (this.nextConfig.i18n) {
const localeResult = normalizeLocalePath(
matchedPath,
this.nextConfig.i18n.locales
)
matchedPath = localeResult.pathname

if (
!isDynamicRoute(srcPathname) &&
!(await this.hasPage(srcPathname))
) {
for (const dynamicRoute of this.dynamicRoutes || []) {
if (dynamicRoute.match(srcPathname)) {
srcPathname = dynamicRoute.page
break
if (localeResult.detectedLocale) {
parsedUrl.query.__nextLocale = localeResult.detectedLocale
}
}
}
matchedPath = denormalizePagePath(matchedPath)
let srcPathname = matchedPath

const pageIsDynamic = isDynamicRoute(srcPathname)
const utils = getUtils({
pageIsDynamic,
page: srcPathname,
i18n: this.nextConfig.i18n,
basePath: this.nextConfig.basePath,
rewrites: this.customRoutes.rewrites,
})
if (
!isDynamicRoute(srcPathname) &&
!(await this.hasPage(srcPathname))
) {
for (const dynamicRoute of this.dynamicRoutes || []) {
if (dynamicRoute.match(srcPathname)) {
srcPathname = dynamicRoute.page
break
}
}
}

try {
const pageIsDynamic = isDynamicRoute(srcPathname)
const utils = getUtils({
pageIsDynamic,
page: srcPathname,
i18n: this.nextConfig.i18n,
basePath: this.nextConfig.basePath,
rewrites: this.customRoutes.rewrites,
})
// ensure parsedUrl.pathname includes URL before processing
// rewrites or they won't match correctly
if (defaultLocale && !pathnameInfo.locale) {
Expand All @@ -523,7 +512,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
if (pageIsDynamic) {
let params: ParsedUrlQuery | false = {}

Object.assign(parsedUrl.query, parsedPath.query)
const paramsResult = utils.normalizeDynamicRouteParams(
parsedUrl.query
)
Expand All @@ -542,27 +530,17 @@ export default abstract class Server<ServerOptions extends Options = Options> {
parsedUrl.query.__nextLocale = opts.locale
}
} else {
params = utils.dynamicRouteMatcher!(matchedPathnameNoExt) || {}
params = utils.dynamicRouteMatcher!(matchedPath) || {}
}

if (params) {
if (!paramsResult.hasValidParams) {
params = utils.normalizeDynamicRouteParams(params).params
}

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

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

Expand All @@ -572,20 +550,17 @@ export default abstract class Server<ServerOptions extends Options = Options> {
...Object.keys(utils.defaultRouteRegex?.groups || {}),
])
}
parsedUrl.pathname = `${this.nextConfig.basePath || ''}${
matchedPath === '/' && this.nextConfig.basePath ? '' : matchedPath
}`
url.pathname = parsedUrl.pathname
} catch (err) {
if (err instanceof DecodeError || err instanceof NormalizeError) {
res.statusCode = 400
return this.renderError(null, req, res, '/_error', {})
}
throw err
}

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

addRequestMeta(req, '__nextHadTrailingSlash', pathnameInfo.trailingSlash)
Expand Down Expand Up @@ -773,18 +748,10 @@ export default abstract class Server<ServerOptions extends Options = Options> {
}
}

const parsedUrl = parseUrl(pathname, true)

await this.render(
req,
res,
pathname,
{ ..._parsedUrl.query, _nextDataReq: '1' },
parsedUrl,
true
)
return {
finished: true,
pathname,
query: { ..._parsedUrl.query, __nextDataReq: '1' },
finished: false,
}
},
},
Expand Down Expand Up @@ -1136,7 +1103,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
if (
!internalRender &&
!this.minimalMode &&
!query._nextDataReq &&
!query.__nextDataReq &&
(req.url?.match(/^\/_next\//) ||
(this.hasStaticDir && req.url!.match(/^\/static\//)))
) {
Expand Down Expand Up @@ -1208,9 +1175,36 @@ export default abstract class Server<ServerOptions extends Options = Options> {

// Toggle whether or not this is a Data request
const isDataReq =
!!query._nextDataReq && (isSSG || hasServerProps || isServerComponent)
!!query.__nextDataReq && (isSSG || hasServerProps || isServerComponent)

delete query._nextDataReq
// normalize req.url for SSG paths as it is not exposed
// to getStaticProps and the asPath should not expose /_next/data
if (
isSSG &&
this.minimalMode &&
req.headers['x-matched-path'] &&
req.url.startsWith('/_next/data')
) {
req.url = this.stripNextDataPath(req.url)
}

if (!!query.__nextDataReq) {
res.setHeader(
'x-nextjs-matched-path',
`${query.__nextLocale ? `/${query.__nextLocale}` : ''}${pathname}`
)
// return empty JSON when not an SSG/SSP page and not an error
if (
!(isSSG || hasServerProps) &&
(!res.statusCode || res.statusCode === 200 || res.statusCode === 404)
) {
res.setHeader('content-type', 'application/json')
res.body('{}')
res.send()
return null
}
}
delete query.__nextDataReq

// Don't delete query.__flight__ yet, it still needs to be used in renderToHTML later
const isFlightRequest = Boolean(
Expand Down Expand Up @@ -1710,7 +1704,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
}
}

private stripNextDataPath(path: string) {
private stripNextDataPath(path: string, stripLocale = true) {
if (path.includes(this.buildId)) {
const splitPath = path.substring(
path.indexOf(this.buildId) + this.buildId.length
Expand All @@ -1719,7 +1713,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
path = denormalizePagePath(splitPath.replace(/\.json$/, ''))
}

if (this.nextConfig.i18n) {
if (this.nextConfig.i18n && stripLocale) {
const { locales } = this.nextConfig.i18n
return normalizeLocalePath(path, locales).pathname
}
Expand Down
3 changes: 3 additions & 0 deletions packages/next/server/dev/next-dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,9 @@ export default class DevServer extends Server {
}))

this.router.setDynamicRoutes(this.dynamicRoutes)
this.router.setCatchallMiddleware(
this.generateCatchAllMiddlewareRoute(true)
)

if (!resolved) {
resolve()
Expand Down
17 changes: 14 additions & 3 deletions packages/next/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,7 @@ export default class NextNodeServer extends BaseServer {
...(components.getStaticProps
? ({
amp: query.amp,
_nextDataReq: query._nextDataReq,
__nextDataReq: query.__nextDataReq,
__nextLocale: query.__nextLocale,
__nextDefaultLocale: query.__nextDefaultLocale,
__flight__: query.__flight__,
Expand Down Expand Up @@ -1116,7 +1116,12 @@ export default class NextNodeServer extends BaseServer {
const normalizedPathname = removeTrailingSlash(params.parsedUrl.pathname)

// For middleware to "fetch" we must always provide an absolute URL
const url = getRequestMeta(params.request, '__NEXT_INIT_URL')!
const query = urlQueryToSearchParams(params.parsed.query).toString()
const locale = params.parsed.query.__nextLocale
const url = `http://${this.hostname}:${this.port}${
locale ? `/${locale}` : ''
}${params.parsed.pathname}${query ? `?${query}` : ''}`

if (!url.startsWith('http')) {
throw new Error(
'To use middleware you must provide a `hostname` and `port` to the Next.js Server'
Expand Down Expand Up @@ -1211,9 +1216,15 @@ export default class NextNodeServer extends BaseServer {
return result
}

protected generateCatchAllMiddlewareRoute(): Route | undefined {
protected generateCatchAllMiddlewareRoute(
devReady?: boolean
): Route | undefined {
if (this.minimalMode) return undefined

if ((!this.renderOpts.dev || devReady) && !this.getMiddleware().length) {
return undefined
}

return {
match: getPathMatch('/:path*'),
type: 'route',
Expand Down
4 changes: 2 additions & 2 deletions packages/next/server/request-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ type NextQueryMetadata = {
__nextLocale?: string
__nextSsgPath?: string
_nextBubbleNoFallback?: '1'
_nextDataReq?: '1'
__nextDataReq?: '1'
}

export type NextParsedUrlQuery = ParsedUrlQuery &
Expand All @@ -80,7 +80,7 @@ export function getNextInternalQuery(
'__nextLocale',
'__nextSsgPath',
'_nextBubbleNoFallback',
'_nextDataReq',
'__nextDataReq',
]
const nextInternalQuery: NextQueryMetadata = {}

Expand Down
8 changes: 7 additions & 1 deletion packages/next/server/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ export default class Router {
setDynamicRoutes(routes: DynamicRoutes = []) {
this.dynamicRoutes = routes
}
setCatchallMiddleware(route?: Route) {
this.catchAllMiddleware = route
}

addFsRoute(fsRoute: Route) {
this.fsRoutes.unshift(fsRoute)
Expand Down Expand Up @@ -208,12 +211,15 @@ export default class Router {
*/

const allRoutes = [
...(this.catchAllMiddleware
? this.fsRoutes.filter((r) => r.name === '_next/data catchall')
: []),
...this.headers,
...this.redirects,
...this.rewrites.beforeFiles,
...(this.useFileSystemPublicRoutes && this.catchAllMiddleware
? [this.catchAllMiddleware]
: []),
...this.rewrites.beforeFiles,
...this.fsRoutes,
// We only check the catch-all route if public page routes hasn't been
// disabled
Expand Down
Loading

0 comments on commit 607ff2b

Please sign in to comment.