Skip to content

Commit

Permalink
Cleanup of /_next/data handling in server (#54689)
Browse files Browse the repository at this point in the history
Previously the code used in both edge and node runtimes used _almost_ the same code. This unifies the two into the base server implementation while also preserving the specific case handled by the split.

This also moves the `/_next/data` route matching to a cached value to prevent it from creating the `RegExp` on every request evaluation.
  • Loading branch information
wyattjoh authored Aug 28, 2023
1 parent 8ed492f commit 540f8e2
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 196 deletions.
115 changes: 108 additions & 7 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ import {
NextRequestAdapter,
signalFromNodeResponse,
} from './web/spec-extension/adapters/next-request'
import { matchNextDataPathname } from './lib/match-next-data-pathname'
import getRouteFromAssetPath from '../shared/lib/router/utils/get-route-from-asset-path'

export type FindComponentsResult = {
components: LoadComponentsReturnType
Expand Down Expand Up @@ -483,11 +485,102 @@ export default abstract class Server<ServerOptions extends Options = Options> {
return this.matchers.reload()
}

protected async normalizeNextData(
_req: BaseNextRequest,
_res: BaseNextResponse,
_parsedUrl: NextUrlWithParsedQuery
protected async handleNextDataRequest(
req: BaseNextRequest,
res: BaseNextResponse,
parsedUrl: NextUrlWithParsedQuery
): Promise<{ finished: boolean }> {
const middleware = this.getMiddleware()
const params = matchNextDataPathname(parsedUrl.pathname)

// ignore for non-next data URLs
if (!params || !params.path) {
return { finished: false }
}

if (params.path[0] !== this.buildId) {
// Ignore if its a middleware request when we aren't on edge.
if (
process.env.NEXT_RUNTIME !== 'edge' &&
req.headers['x-middleware-invoke']
) {
return { finished: false }
}

// Make sure to 404 if the buildId isn't correct
await this.render404(req, res, parsedUrl)
return { finished: true }
}

// remove buildId from URL
params.path.shift()

const lastParam = params.path[params.path.length - 1]

// show 404 if it doesn't end with .json
if (typeof lastParam !== 'string' || !lastParam.endsWith('.json')) {
await this.render404(req, res, parsedUrl)
return {
finished: true,
}
}

// re-create page's pathname
let pathname = `/${params.path.join('/')}`
pathname = getRouteFromAssetPath(pathname, '.json')

// ensure trailing slash is normalized per config
if (middleware) {
if (this.nextConfig.trailingSlash && !pathname.endsWith('/')) {
pathname += '/'
}
if (
!this.nextConfig.trailingSlash &&
pathname.length > 1 &&
pathname.endsWith('/')
) {
pathname = pathname.substring(0, pathname.length - 1)
}
}

if (this.i18nProvider) {
// Remove the port from the hostname if present.
const hostname = req?.headers.host?.split(':')[0].toLowerCase()

const domainLocale = this.i18nProvider.detectDomainLocale(hostname)
const defaultLocale =
domainLocale?.defaultLocale ?? this.i18nProvider.config.defaultLocale

const localePathResult = this.i18nProvider.analyze(pathname)

// If the locale is detected from the path, we need to remove it
// from the pathname.
if (localePathResult.detectedLocale) {
pathname = localePathResult.pathname
}

// Update the query with the detected locale and default locale.
parsedUrl.query.__nextLocale = localePathResult.detectedLocale
parsedUrl.query.__nextDefaultLocale = defaultLocale

// If the locale is not detected from the path, we need to mark that
// it was not inferred from default.
if (!localePathResult.detectedLocale) {
delete parsedUrl.query.__nextInferredLocaleFromDefault
}

// If no locale was detected and we don't have middleware, we need
// to render a 404 page.
if (!localePathResult.detectedLocale && !middleware) {
parsedUrl.query.__nextLocale = defaultLocale
await this.render404(req, res, parsedUrl)
return { finished: true }
}
}

parsedUrl.pathname = pathname
parsedUrl.query.__nextDataReq = '1'

return { finished: false }
}

Expand Down Expand Up @@ -942,7 +1035,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
parsedUrl.pathname = matchedPath
url.pathname = parsedUrl.pathname

const normalizeResult = await this.normalizeNextData(
const normalizeResult = await this.handleNextDataRequest(
req,
res,
parsedUrl
Expand Down Expand Up @@ -1125,7 +1218,11 @@ export default abstract class Server<ServerOptions extends Options = Options> {
return
}
}
const nextDataResult = await this.normalizeNextData(req, res, parsedUrl)
const nextDataResult = await this.handleNextDataRequest(
req,
res,
parsedUrl
)

if (nextDataResult.finished) {
return
Expand All @@ -1138,7 +1235,11 @@ export default abstract class Server<ServerOptions extends Options = Options> {
process.env.NEXT_RUNTIME !== 'edge' &&
req.headers['x-middleware-invoke']
) {
const nextDataResult = await this.normalizeNextData(req, res, parsedUrl)
const nextDataResult = await this.handleNextDataRequest(
req,
res,
parsedUrl
)

if (nextDataResult.finished) {
return
Expand Down
9 changes: 9 additions & 0 deletions packages/next/src/server/lib/match-next-data-pathname.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { getPathMatch } from '../../shared/lib/router/utils/path-match'

const matcher = getPathMatch('/_next/data/:path*')

export function matchNextDataPathname(pathname: string | null | undefined) {
if (typeof pathname !== 'string') return false

return matcher(pathname)
}
106 changes: 0 additions & 106 deletions packages/next/src/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ import {
} from '../shared/lib/constants'
import { findDir } from '../lib/find-pages-dir'
import { UrlWithParsedQuery } from 'url'
import { getPathMatch } from '../shared/lib/router/utils/path-match'
import getRouteFromAssetPath from '../shared/lib/router/utils/get-route-from-asset-path'
import { NodeNextRequest, NodeNextResponse } from './base-http/node'
import { sendRenderResult } from './send-payload'
import { getExtension, serveStatic } from './serve-static'
Expand Down Expand Up @@ -705,110 +703,6 @@ export default class NextNodeServer extends BaseServer {
return html.toString('utf8')
}

protected async normalizeNextData(
req: BaseNextRequest,
res: BaseNextResponse,
parsedUrl: NextUrlWithParsedQuery
) {
const params = getPathMatch('/_next/data/:path*')(parsedUrl.pathname)

// ignore for non-next data URLs
if (!params || !params.path) {
return {
finished: false,
}
}

if (params.path[0] !== this.buildId) {
// ignore if its a middleware request
if (req.headers['x-middleware-invoke']) {
return {
finished: false,
}
}

// Make sure to 404 if the buildId isn't correct
await this.render404(req, res, parsedUrl)
return {
finished: true,
}
}

// remove buildId from URL
params.path.shift()

const lastParam = params.path[params.path.length - 1]

// show 404 if it doesn't end with .json
if (typeof lastParam !== 'string' || !lastParam.endsWith('.json')) {
await this.render404(req, res, parsedUrl)
return {
finished: true,
}
}

// re-create page's pathname
let pathname = `/${params.path.join('/')}`
pathname = getRouteFromAssetPath(pathname, '.json')

// ensure trailing slash is normalized per config
if (this.getMiddleware()) {
if (this.nextConfig.trailingSlash && !pathname.endsWith('/')) {
pathname += '/'
}
if (
!this.nextConfig.trailingSlash &&
pathname.length > 1 &&
pathname.endsWith('/')
) {
pathname = pathname.substring(0, pathname.length - 1)
}
}

if (this.i18nProvider) {
// Remove the port from the hostname if present.
const hostname = req?.headers.host?.split(':')[0].toLowerCase()

const domainLocale = this.i18nProvider.detectDomainLocale(hostname)
const defaultLocale =
domainLocale?.defaultLocale ?? this.i18nProvider.config.defaultLocale

const localePathResult = this.i18nProvider.analyze(pathname)

// If the locale is detected from the path, we need to remove it
// from the pathname.
if (localePathResult.detectedLocale) {
pathname = localePathResult.pathname
}

// Update the query with the detected locale and default locale.
parsedUrl.query.__nextLocale = localePathResult.detectedLocale
parsedUrl.query.__nextDefaultLocale = defaultLocale

// If the locale is not detected from the path, we need to mark that
// it was not inferred from default.
if (!parsedUrl.query.__nextLocale) {
delete parsedUrl.query.__nextInferredLocaleFromDefault
}

// If no locale was detected and we don't have middleware, we need
// to render a 404 page.
// NOTE: (wyattjoh) we may need to change this for app/
if (!localePathResult.detectedLocale && !this.getMiddleware()) {
parsedUrl.query.__nextLocale = defaultLocale
await this.render404(req, res, parsedUrl)
return { finished: true }
}
}

parsedUrl.pathname = pathname
parsedUrl.query.__nextDataReq = '1'

return {
finished: false,
}
}

protected async handleNextImageRequest(
req: BaseNextRequest,
res: BaseNextResponse,
Expand Down
83 changes: 0 additions & 83 deletions packages/next/src/server/web-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ import { generateETag } from './lib/etag'
import { addRequestMeta } from './request-meta'
import WebResponseCache from './response-cache/web'
import { isAPIRoute } from '../lib/is-api-route'
import { getPathMatch } from '../shared/lib/router/utils/path-match'
import getRouteFromAssetPath from '../shared/lib/router/utils/get-route-from-asset-path'
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash'
import { isDynamicRoute } from '../shared/lib/router/utils'
import { interpolateDynamicPath, normalizeVercelUrl } from './server-utils'
Expand Down Expand Up @@ -169,86 +166,6 @@ export default class NextWebServer extends BaseServer<WebServerOptions> {
return this.serverOptions.webServerConfig.extendRenderOpts.nextFontManifest
}

protected async normalizeNextData(
req: BaseNextRequest,
res: BaseNextResponse,
parsedUrl: NextUrlWithParsedQuery
): Promise<{ finished: boolean }> {
const middleware = this.getMiddleware()
const params = getPathMatch('/_next/data/:path*')(parsedUrl.pathname)

// Make sure to 404 for /_next/data/ itself and
// we also want to 404 if the buildId isn't correct
if (!params || !params.path || params.path[0] !== this.buildId) {
await this.render404(req, res, parsedUrl)
return {
finished: true,
}
}
// remove buildId from URL
params.path.shift()

const lastParam = params.path[params.path.length - 1]

// show 404 if it doesn't end with .json
if (typeof lastParam !== 'string' || !lastParam.endsWith('.json')) {
await this.render404(req, res, parsedUrl)
return {
finished: true,
}
}

// re-create page's pathname
let pathname = `/${params.path.join('/')}`
pathname = getRouteFromAssetPath(pathname, '.json')

// ensure trailing slash is normalized per config
if (middleware) {
if (this.nextConfig.trailingSlash && !pathname.endsWith('/')) {
pathname += '/'
}
if (
!this.nextConfig.trailingSlash &&
pathname.length > 1 &&
pathname.endsWith('/')
) {
pathname = pathname.substring(0, pathname.length - 1)
}
}

if (this.nextConfig.i18n) {
const { host } = req?.headers || {}
// remove port from host and remove port if present
const hostname = host?.split(':')[0].toLowerCase()
const localePathResult = normalizeLocalePath(
pathname,
this.nextConfig.i18n.locales
)
const domainLocale = this.i18nProvider?.detectDomainLocale(hostname)

let detectedLocale = ''

if (localePathResult.detectedLocale) {
pathname = localePathResult.pathname
detectedLocale = localePathResult.detectedLocale
}

parsedUrl.query.__nextLocale = detectedLocale
parsedUrl.query.__nextDefaultLocale =
domainLocale?.defaultLocale || this.nextConfig.i18n.defaultLocale

if (!detectedLocale && !middleware) {
parsedUrl.query.__nextLocale = parsedUrl.query.__nextDefaultLocale
await this.render404(req, res, parsedUrl)
return { finished: true }
}
}
parsedUrl.pathname = pathname
parsedUrl.query.__nextDataReq = '1'

return { finished: false }
}

protected async handleCatchallRenderRequest(
req: BaseNextRequest,
res: BaseNextResponse,
Expand Down

0 comments on commit 540f8e2

Please sign in to comment.