import '../../build/adapter/setup-node-env.external'
import type { IncomingMessage, ServerResponse } from 'node:http'
import type {
InstrumentationOnRequestError,
RequestErrorContext,
} from '../instrumentation/types'
import type { ParsedUrlQuery } from 'node:querystring'
import type { UrlWithParsedQuery } from 'node:url'
import type {
PrerenderManifest,
RequiredServerFilesManifest,
} from '../../build'
import type { DevRoutesManifest } from '../lib/router-utils/setup-dev-bundler'
import type { RouteDefinition } from '../route-definitions/route-definition'
import type { DeepReadonly } from '../../shared/lib/deep-readonly'
import {
BUILD_ID_FILE,
BUILD_MANIFEST,
CLIENT_REFERENCE_MANIFEST,
DYNAMIC_CSS_MANIFEST,
NEXT_FONT_MANIFEST,
PREFETCH_HINTS,
PRERENDER_MANIFEST,
REACT_LOADABLE_MANIFEST,
ROUTES_MANIFEST,
SERVER_FILES_MANIFEST,
SERVER_REFERENCE_MANIFEST,
SUBRESOURCE_INTEGRITY_MANIFEST,
} from '../../shared/lib/constants'
import { parseReqUrl } from '../../lib/url'
import {
normalizeLocalePath,
type PathLocale,
} from '../../shared/lib/i18n/normalize-locale-path'
import { isDynamicRoute } from '../../shared/lib/router/utils'
import { removePathPrefix } from '../../shared/lib/router/utils/remove-path-prefix'
import { getServerUtils } from '../server-utils'
import { detectDomainLocale } from '../../shared/lib/i18n/detect-domain-locale'
import { getHostname } from '../../shared/lib/get-hostname'
import { checkIsOnDemandRevalidate } from '../api-utils'
import type { PreviewData } from '../../types'
import type { BuildManifest } from '../get-page-files'
import type { ReactLoadableManifest } from '../load-components'
import type { NextFontManifest } from '../../build/webpack/plugins/next-font-manifest-plugin'
import { normalizeDataPath } from '../../shared/lib/page-path/normalize-data-path'
import { pathHasPrefix } from '../../shared/lib/router/utils/path-has-prefix'
import {
addRequestMeta,
getRequestMeta,
type NextIncomingMessage,
} from '../request-meta'
import { patchSetHeaderWithCookieSupport } from '../lib/patch-set-header'
import { normalizePagePath } from '../../shared/lib/page-path/normalize-page-path'
import { isStaticMetadataRoute } from '../../lib/metadata/is-metadata-route'
import { IncrementalCache } from '../lib/incremental-cache'
import { initializeCacheHandlers, setCacheHandler } from '../use-cache/handlers'
import { interopDefault } from '../app-render/interop-default'
import { RouteKind } from '../route-kind'
import type { BaseNextRequest } from '../base-http'
import type { I18NConfig, NextConfigRuntime } from '../config-shared'
import ResponseCache, { type ResponseGenerator } from '../response-cache'
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
import {
RouterServerContextSymbol,
routerServerGlobal,
type RouterServerContext,
} from '../lib/router-utils/router-server-context'
import { decodePathParams } from '../lib/router-utils/decode-path-params'
import { removeTrailingSlash } from '../../shared/lib/router/utils/remove-trailing-slash'
import { isInterceptionRouteRewrite } from '../../lib/is-interception-route-rewrite'
/**
* RouteModuleOptions is the options that are passed to the route module, other
* route modules should extend this class to add specific options for their
* route.
*/
export interface RouteModuleOptions<
D extends RouteDefinition = RouteDefinition,
U = unknown,
> {
readonly definition: Readonly<D>
readonly userland: Readonly<U>
readonly distDir: string
readonly relativeProjectDir: string
}
/**
* RouteHandlerContext is the base context for a route handler.
*/
export interface RouteModuleHandleContext {
/**
* Any matched parameters for the request. This is only defined for dynamic
* routes.
*/
params: Record<string, string | string[] | undefined> | undefined
}
const dynamicImportEsmDefault = (id: string) =>
import(/* webpackIgnore: true */ /* turbopackIgnore: true */ id).then(
(mod) => mod.default || mod
)
/**
* RouteModule is the base class for all route modules. This class should be
* extended by all route modules.
*/
export abstract class RouteModule<
D extends RouteDefinition = RouteDefinition,
U = unknown,
> {
/**
* The userland module. This is the module that is exported from the user's
* code. Exposed as a getter so subclasses can override with lazy loading.
*/
protected _userland: Readonly<U>
get userland(): Readonly<U> {
return this._userland
}
/**
* The definition of the route.
*/
public readonly definition: Readonly<D>
/**
* The shared modules that are exposed and required for the route module.
*/
public static readonly sharedModules: any
public isDev: boolean
public distDir: string
public relativeProjectDir: string
public incrementCache?: IncrementalCache
public responseCache?: ResponseCache
constructor({
userland,
definition,
distDir,
relativeProjectDir,
}: RouteModuleOptions<D, U>) {
this._userland = userland
this.definition = definition
this.isDev = !!process.env.__NEXT_DEV_SERVER
this.distDir = distDir
this.relativeProjectDir = relativeProjectDir
}
private getRouterServerContext(
req: NextIncomingMessage
): RouterServerContext[string] | undefined {
const hostname = getRequestMeta(req, 'hostname')
const revalidate = getRequestMeta(req, 'revalidate')
const render404 = getRequestMeta(req, 'render404')
const relativeProjectDir =
getRequestMeta(req, 'relativeProjectDir') || this.relativeProjectDir
const routerServerContext =
routerServerGlobal[RouterServerContextSymbol]?.[relativeProjectDir]
return {
...routerServerContext,
...(hostname !== undefined ? { hostname } : {}),
...(revalidate !== undefined ? { revalidate } : {}),
...(render404 !== undefined ? { render404 } : {}),
}
}
public normalizeUrl(
_req: IncomingMessage | BaseNextRequest,
_parsedUrl: UrlWithParsedQuery
) {}
public async instrumentationOnRequestError(
req: IncomingMessage | BaseNextRequest,
...args: Parameters<InstrumentationOnRequestError>
) {
if (process.env.NEXT_RUNTIME === 'edge') {
const { getEdgeInstrumentationModule } = await import('../web/globals')
const instrumentation = await getEdgeInstrumentationModule()
if (instrumentation) {
await instrumentation.onRequestError?.(...args)
}
} else {
const { join } = require('node:path') as typeof import('node:path')
const absoluteProjectDir = join(
/* turbopackIgnore: true */
process.cwd(),
getRequestMeta(req, 'relativeProjectDir') || this.relativeProjectDir
)
const { instrumentationOnRequestError } = await import(
'../lib/router-utils/instrumentation-globals.external.js'
)
return instrumentationOnRequestError(
absoluteProjectDir,
this.distDir,
...args
)
}
}
private loadManifests(
srcPage: string,
projectDir?: string
): {
buildId: string
buildManifest: BuildManifest
fallbackBuildManifest: BuildManifest
routesManifest: DeepReadonly<DevRoutesManifest>
nextFontManifest: DeepReadonly<NextFontManifest>
prerenderManifest: DeepReadonly<PrerenderManifest>
serverFilesManifest: DeepReadonly<RequiredServerFilesManifest> | undefined
reactLoadableManifest: DeepReadonly<ReactLoadableManifest>
subresourceIntegrityManifest: any
clientReferenceManifest: any
serverActionsManifest: any
dynamicCssManifest: any
prefetchHintsManifest: Record<string, any> | undefined
interceptionRoutePatterns: RegExp[]
} {
let result
if (process.env.NEXT_RUNTIME === 'edge') {
const { getEdgePreviewProps } =
require('../web/get-edge-preview-props') as typeof import('../web/get-edge-preview-props')
const maybeJSONParse = (str?: string) =>
str ? JSON.parse(str) : undefined
result = {
buildId: process.env.__NEXT_BUILD_ID || '',
buildManifest: self.__BUILD_MANIFEST as any,
fallbackBuildManifest: {} as any,
reactLoadableManifest: maybeJSONParse(self.__REACT_LOADABLE_MANIFEST),
nextFontManifest: maybeJSONParse(self.__NEXT_FONT_MANIFEST),
prerenderManifest: {
routes: {},
dynamicRoutes: {},
notFoundRoutes: [],
version: 4,
preview: getEdgePreviewProps(),
} as const,
routesManifest: {
version: 4,
caseSensitive: Boolean(process.env.__NEXT_CASE_SENSITIVE_ROUTES),
basePath: process.env.__NEXT_BASE_PATH || '',
rewrites: (process.env.__NEXT_REWRITES as any) || {
beforeFiles: [],
afterFiles: [],
fallback: [],
},
redirects: [],
headers: [],
onMatchHeaders: [],
i18n:
(process.env.__NEXT_I18N_CONFIG as any as I18NConfig) || undefined,
skipProxyUrlNormalize: Boolean(
process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE
),
},
serverFilesManifest: self.__SERVER_FILES_MANIFEST,
clientReferenceManifest: self.__RSC_MANIFEST?.[srcPage],
serverActionsManifest: maybeJSONParse(self.__RSC_SERVER_MANIFEST),
subresourceIntegrityManifest: maybeJSONParse(
self.__SUBRESOURCE_INTEGRITY_MANIFEST
),
dynamicCssManifest: maybeJSONParse(self.__DYNAMIC_CSS_MANIFEST),
// Edge pages are always dynamic so prefetch inlining hints
// don't apply. The runtime handles missing hints gracefully.
prefetchHintsManifest: undefined,
interceptionRoutePatterns: (
maybeJSONParse(self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST) ?? []
).map((rewrite: any) => new RegExp(rewrite.regex)),
}
} else {
if (!projectDir) {
throw new Error('Invariant: projectDir is required for node runtime')
}
const { loadManifestFromRelativePath } =
require('../load-manifest.external') as typeof import('../load-manifest.external')
const normalizedPagePath = normalizePagePath(srcPage)
const router =
this.definition.kind === RouteKind.PAGES ||
this.definition.kind === RouteKind.PAGES_API
? 'pages'
: 'app'
const [
routesManifest,
prerenderManifest,
buildManifest,
fallbackBuildManifest,
reactLoadableManifest,
nextFontManifest,
clientReferenceManifest,
serverActionsManifest,
subresourceIntegrityManifest,
serverFilesManifest,
buildId,
dynamicCssManifest,
prefetchHintsManifest,
] = [
loadManifestFromRelativePath<DevRoutesManifest>({
projectDir,
distDir: this.distDir,
manifest: ROUTES_MANIFEST,
shouldCache: !this.isDev,
}),
loadManifestFromRelativePath<PrerenderManifest>({
projectDir,
distDir: this.distDir,
manifest: PRERENDER_MANIFEST,
shouldCache: !this.isDev,
}),
loadManifestFromRelativePath<BuildManifest>({
projectDir,
distDir: this.distDir,
manifest: BUILD_MANIFEST,
shouldCache: !this.isDev,
}),
srcPage === '/_error'
? loadManifestFromRelativePath<BuildManifest>({
projectDir,
distDir: this.distDir,
manifest: `fallback-${BUILD_MANIFEST}`,
shouldCache: !this.isDev,
handleMissing: true,
})
: ({} as BuildManifest),
loadManifestFromRelativePath<ReactLoadableManifest>({
projectDir,
distDir: this.distDir,
manifest: process.env.TURBOPACK
? `server/${router === 'app' ? 'app' : 'pages'}${normalizedPagePath}/${REACT_LOADABLE_MANIFEST}`
: REACT_LOADABLE_MANIFEST,
handleMissing: true,
shouldCache: !this.isDev,
}),
loadManifestFromRelativePath<NextFontManifest>({
projectDir,
distDir: this.distDir,
manifest: `server/${NEXT_FONT_MANIFEST}.json`,
shouldCache: !this.isDev,
}),
router === 'app' && !isStaticMetadataRoute(srcPage)
? loadManifestFromRelativePath({
distDir: this.distDir,
projectDir,
useEval: true,
handleMissing: true,
manifest: `server/app${srcPage.replace(/%5F/g, '_') + '_' + CLIENT_REFERENCE_MANIFEST}.js`,
shouldCache: !this.isDev,
})
: undefined,
router === 'app'
? loadManifestFromRelativePath<any>({
distDir: this.distDir,
projectDir,
manifest: `server/${SERVER_REFERENCE_MANIFEST}.json`,
handleMissing: true,
shouldCache: !this.isDev,
})
: {},
loadManifestFromRelativePath<Record<string, string>>({
projectDir,
distDir: this.distDir,
manifest: `server/${SUBRESOURCE_INTEGRITY_MANIFEST}.json`,
handleMissing: true,
shouldCache: !this.isDev,
}),
this.isDev
? undefined
: loadManifestFromRelativePath<RequiredServerFilesManifest>({
projectDir,
distDir: this.distDir,
shouldCache: true,
manifest: `${SERVER_FILES_MANIFEST}.json`,
}),
this.isDev
? 'development'
: loadManifestFromRelativePath<any>({
projectDir,
distDir: this.distDir,
manifest: BUILD_ID_FILE,
skipParse: true,
shouldCache: true,
}),
loadManifestFromRelativePath<any>({
projectDir,
distDir: this.distDir,
manifest: DYNAMIC_CSS_MANIFEST,
shouldCache: !this.isDev,
handleMissing: true,
}),
router === 'app'
? loadManifestFromRelativePath<Record<string, any>>({
projectDir,
distDir: this.distDir,
manifest: `server/${PREFETCH_HINTS}`,
shouldCache: !this.isDev,
handleMissing: true,
})
: undefined,
]
result = {
buildId,
buildManifest,
fallbackBuildManifest,
routesManifest,
nextFontManifest,
prerenderManifest,
serverFilesManifest,
reactLoadableManifest,
clientReferenceManifest: (clientReferenceManifest as any)
?.__RSC_MANIFEST?.[srcPage.replace(/%5F/g, '_')],
serverActionsManifest,
subresourceIntegrityManifest,
dynamicCssManifest,
prefetchHintsManifest,
interceptionRoutePatterns: routesManifest.rewrites.beforeFiles
.filter(isInterceptionRouteRewrite)
.map((rewrite) => new RegExp(rewrite.regex)),
}
}
return result
}
public async loadCustomCacheHandlers(
req: IncomingMessage | BaseNextRequest,
nextConfig: NextConfigRuntime
) {
if (process.env.NEXT_RUNTIME !== 'edge') {
const { cacheMaxMemorySize, cacheHandlers } = nextConfig
if (!cacheHandlers) return
// If we've already initialized the cache handlers interface, don't do it
// again.
if (!initializeCacheHandlers(cacheMaxMemorySize)) return
for (const [kind, handler] of Object.entries(cacheHandlers)) {
if (!handler) continue
const { formatDynamicImportPath } =
require('../../lib/format-dynamic-import-path') as typeof import('../../lib/format-dynamic-import-path')
const { join } = require('node:path') as typeof import('node:path')
const absoluteProjectDir = join(
/* turbopackIgnore: true */
process.cwd(),
getRequestMeta(req, 'relativeProjectDir') || this.relativeProjectDir
)
setCacheHandler(
kind,
interopDefault(
await dynamicImportEsmDefault(
formatDynamicImportPath(
`${absoluteProjectDir}/${this.distDir}`,
handler
)
)
)
)
}
}
}
public async getIncrementalCache(
req: IncomingMessage | BaseNextRequest,
nextConfig: NextConfigRuntime,
prerenderManifest: DeepReadonly<PrerenderManifest>,
isMinimalMode: boolean
): Promise<IncrementalCache> {
if (process.env.NEXT_RUNTIME === 'edge') {
return (globalThis as any).__incrementalCache
} else {
let CacheHandler: any
const { cacheHandler } = nextConfig
if (cacheHandler) {
const { formatDynamicImportPath } =
require('../../lib/format-dynamic-import-path') as typeof import('../../lib/format-dynamic-import-path')
CacheHandler = interopDefault(
await dynamicImportEsmDefault(
formatDynamicImportPath(this.distDir, cacheHandler)
)
)
}
const { join } = require('node:path') as typeof import('node:path')
const projectDir = join(
/* turbopackIgnore: true */
process.cwd(),
getRequestMeta(req, 'relativeProjectDir') || this.relativeProjectDir
)
await this.loadCustomCacheHandlers(req, nextConfig)
// incremental-cache is request specific
// although can have shared caches in module scope
// per-cache handler
const incrementalCache = new IncrementalCache({
fs: (
require('../lib/node-fs-methods') as typeof import('../lib/node-fs-methods')
).nodeFs,
dev: this.isDev,
requestHeaders: req.headers,
allowedRevalidateHeaderKeys:
nextConfig.experimental.allowedRevalidateHeaderKeys,
minimalMode: isMinimalMode,
serverDistDir: `${projectDir}/${this.distDir}/server`,
fetchCacheKeyPrefix: nextConfig.experimental.fetchCacheKeyPrefix,
maxMemoryCacheSize: nextConfig.cacheMaxMemorySize,
flushToDisk: !isMinimalMode && nextConfig.experimental.isrFlushToDisk,
getPrerenderManifest: () => prerenderManifest,
CurCacheHandler: CacheHandler,
})
// we need to expose this on globalThis as the app-render
// workStore grabs the incrementalCache from there
;(globalThis as any).__incrementalCache = incrementalCache
return incrementalCache
}
}
public async onRequestError(
req: IncomingMessage | BaseNextRequest,
err: unknown,
errorContext: RequestErrorContext,
silenceLog: boolean,
routerServerContext?: RouterServerContext[string]
) {
if (!silenceLog) {
if (routerServerContext?.logErrorWithOriginalStack) {
routerServerContext.logErrorWithOriginalStack(err, 'app-dir')
} else {
console.error(err)
}
}
await this.instrumentationOnRequestError(
req,
err,
{
path: req.url || '/',
headers: req.headers,
method: req.method || 'GET',
},
errorContext
)
}
/** A more lightweight version of `prepare()` for only retrieving the config on edge */
public getNextConfigEdge(req: NextIncomingMessage): {
nextConfig: NextConfigRuntime
deploymentId: string
} {
if (process.env.NEXT_RUNTIME !== 'edge') {
throw new Error(
'Invariant: getNextConfigEdge must only be called in edge runtime'
)
}
let serverFilesManifest = self.__SERVER_FILES_MANIFEST as any as
| RequiredServerFilesManifest
| undefined
const routerServerContext = this.getRouterServerContext(req)
const nextConfig =
routerServerContext?.nextConfig || serverFilesManifest?.config
if (!nextConfig) {
throw new Error("Invariant: nextConfig couldn't be loaded")
}
let deploymentId
if (nextConfig.experimental?.runtimeServerDeploymentId) {
if (!process.env.NEXT_DEPLOYMENT_ID) {
throw new Error(
'process.env.NEXT_DEPLOYMENT_ID is missing but runtimeServerDeploymentId is enabled'
)
}
deploymentId = process.env.NEXT_DEPLOYMENT_ID
} else {
deploymentId = nextConfig.deploymentId || ''
}
return { nextConfig, deploymentId }
}
public async prepare(
req: IncomingMessage | BaseNextRequest,
res: ServerResponse | null,
{
srcPage,
multiZoneDraftMode,
}: {
srcPage: string
multiZoneDraftMode?: boolean
}
): Promise<
| {
buildId: string
deploymentId: string
clientAssetToken: string
locale?: string
locales?: readonly string[]
defaultLocale?: string
query: ParsedUrlQuery
originalQuery: ParsedUrlQuery
originalPathname: string
params?: ParsedUrlQuery
parsedUrl: UrlWithParsedQuery
previewData: PreviewData
pageIsDynamic: boolean
isDraftMode: boolean
resolvedPathname: string
encodedResolvedPathname: string
isNextDataRequest: boolean
buildManifest: DeepReadonly<BuildManifest>
fallbackBuildManifest: DeepReadonly<BuildManifest>
nextFontManifest: DeepReadonly<NextFontManifest>
serverFilesManifest:
| DeepReadonly<RequiredServerFilesManifest>
| undefined
reactLoadableManifest: DeepReadonly<ReactLoadableManifest>
routesManifest: DeepReadonly<DevRoutesManifest>
prerenderManifest: DeepReadonly<PrerenderManifest>
// we can't pull in the client reference type or it causes issues with
// our pre-compiled types
clientReferenceManifest?: any
serverActionsManifest?: any
dynamicCssManifest?: any
prefetchHintsManifest?: Record<string, any>
subresourceIntegrityManifest?: DeepReadonly<Record<string, string>>
isOnDemandRevalidate: boolean
revalidateOnlyGenerated: boolean
nextConfig: NextConfigRuntime
routerServerContext?: RouterServerContext[string]
interceptionRoutePatterns?: any
}
| undefined
> {
let absoluteProjectDir: string | undefined
// edge runtime handles loading instrumentation at the edge adapter level
if (process.env.NEXT_RUNTIME !== 'edge') {
if (res) {
patchSetHeaderWithCookieSupport(req, res)
}
const { join, relative } =
require('node:path') as typeof import('node:path')
absoluteProjectDir = join(
/* turbopackIgnore: true */
process.cwd(),
getRequestMeta(req, 'relativeProjectDir') || this.relativeProjectDir
)
const absoluteDistDir = getRequestMeta(req, 'distDir')
if (absoluteDistDir) {
this.distDir = relative(absoluteProjectDir, absoluteDistDir)
}
const { ensureInstrumentationRegistered } = await import(
'../lib/router-utils/instrumentation-globals.external.js'
)
// ensure instrumentation is registered and pass
// onRequestError below
ensureInstrumentationRegistered(absoluteProjectDir, this.distDir)
}
const manifests = this.loadManifests(srcPage, absoluteProjectDir)
const { routesManifest, prerenderManifest, serverFilesManifest } = manifests
const { basePath, i18n, rewrites } = routesManifest
const routerServerContext = this.getRouterServerContext(req)
const nextConfig =
routerServerContext?.nextConfig || serverFilesManifest?.config
// Injected in base-server.ts
const protocol = req.headers['x-forwarded-proto']?.includes('https')
? 'https'
: 'http'
// When there are hostname and port we build an absolute URL
if (!getRequestMeta(req, 'initURL')) {
const initUrl = serverFilesManifest?.config.experimental.trustHostHeader
? `${protocol}://${req.headers.host || 'localhost'}${req.url}`
: `${protocol}://${routerServerContext?.hostname || 'localhost'}${req.url}`
addRequestMeta(req, 'initURL', initUrl)
addRequestMeta(req, 'initProtocol', protocol)
}
if (basePath) {
req.url = removePathPrefix(req.url || '/', basePath)
}
const parsedUrl = parseReqUrl(req.url || '/')
addRequestMeta(req, 'initQuery', { ...parsedUrl?.query })
// if we couldn't parse the URL we can't continue
if (!parsedUrl) {
return
}
let isNextDataRequest = false
if (pathHasPrefix(parsedUrl.pathname || '/', '/_next/data')) {
isNextDataRequest = true
parsedUrl.pathname = normalizeDataPath(parsedUrl.pathname || '/')
}
this.normalizeUrl(req, parsedUrl)
let originalPathname = parsedUrl.pathname || '/'
const originalQuery = { ...parsedUrl.query }
const pageIsDynamic = isDynamicRoute(srcPage)
let localeResult: PathLocale | undefined
let detectedLocale: string | undefined
if (i18n) {
localeResult = normalizeLocalePath(
parsedUrl.pathname || '/',
i18n.locales
)
if (localeResult.detectedLocale) {
req.url = `${localeResult.pathname}${parsedUrl.search}`
originalPathname = localeResult.pathname
if (!detectedLocale) {
detectedLocale = localeResult.detectedLocale
}
}
}
// Normalize the page path for route matching. The srcPage contains the
// internal page path (e.g., /app/[slug]/page), but route matchers expect
// the pathname format (e.g., /app/[slug]).
const normalizedSrcPage = normalizeAppPath(srcPage)
const serverUtils = getServerUtils({
page: normalizedSrcPage,
i18n,
basePath,
rewrites,
pageIsDynamic,
trailingSlash: process.env.__NEXT_TRAILING_SLASH as any as boolean,
caseSensitive: Boolean(routesManifest.caseSensitive),
})
const domainLocale = detectDomainLocale(
i18n?.domains,
getHostname(parsedUrl, req.headers),
detectedLocale
)
if (Boolean(domainLocale)) {
addRequestMeta(req, 'isLocaleDomain', Boolean(domainLocale))
}
const defaultLocale =
getRequestMeta(req, 'defaultLocale') ||
domainLocale?.defaultLocale ||
i18n?.defaultLocale
// Ensure parsedUrl.pathname includes locale before processing
// rewrites or they won't match correctly.
if (defaultLocale && !detectedLocale) {
parsedUrl.pathname = `/${defaultLocale}${parsedUrl.pathname === '/' ? '' : parsedUrl.pathname}`
}
const locale =
getRequestMeta(req, 'locale') || detectedLocale || defaultLocale
// we apply rewrites against cloned URL so that we don't
// modify the original with the rewrite destination
const { rewriteParams, rewrittenParsedUrl } = serverUtils.handleRewrites(
req,
parsedUrl
)
const rewriteParamKeys = Object.keys(rewriteParams)
Object.assign(parsedUrl.query, rewrittenParsedUrl.query)
// after processing rewrites we want to remove locale
// from parsedUrl pathname
if (i18n) {
parsedUrl.pathname = normalizeLocalePath(
parsedUrl.pathname || '/',
i18n.locales
).pathname
rewrittenParsedUrl.pathname = normalizeLocalePath(
rewrittenParsedUrl.pathname || '/',
i18n.locales
).pathname
}
let params: Record<string, undefined | string | string[]> | undefined =
getRequestMeta(req, 'params')
// attempt parsing from pathname
if (!params && serverUtils.dynamicRouteMatcher) {
const paramsMatch = serverUtils.dynamicRouteMatcher(
normalizeDataPath(
rewrittenParsedUrl?.pathname || parsedUrl.pathname || '/'
)
)
const paramsResult = serverUtils.normalizeDynamicRouteParams(
paramsMatch || {},
true
)
if (paramsResult.hasValidParams) {
params = paramsResult.params
}
}
// Local "next start" expects the routing parsed query values
// to not be present in the URL although when deployed proxies
// will add query values from resolving the routes to pass to function.
// TODO: do we want to change expectations for "next start"
// to include these query values in the URL which affects asPath
// but would match deployed behavior, e.g. a rewrite from middleware
// that adds a query param would be in asPath as query but locally
// it won't be in the asPath but still available in the query object
const query = getRequestMeta(req, 'query') || {
...parsedUrl.query,
}
const routeParamKeys = new Set<string>()
const combinedParamKeys = []
// We don't include rewriteParamKeys in the combinedParamKeys
// for app router since the searchParams is populated from the
// URL so we don't want to strip the rewrite params from the URL
// so that searchParams can include them.
if (
this.definition.kind === RouteKind.PAGES ||
this.definition.kind === RouteKind.PAGES_API
) {
for (const key of [
...rewriteParamKeys,
...Object.keys(serverUtils.defaultRouteMatches || {}),
]) {
// We only want to filter rewrite param keys from the URL
// if they are matches from the URL e.g. the key/value matches
// before and after applying the rewrites /:path for /hello and
// { path: 'hello' } but not for { path: 'another' } and /hello
// TODO: we should prefix rewrite param keys the same as we do
// for dynamic routes so we can identify them properly
const originalValue = Array.isArray(originalQuery[key])
? originalQuery[key].join('')
: originalQuery[key]
const queryValue = Array.isArray(query[key])
? query[key].join('')
: query[key]
if (!(key in originalQuery) || originalValue === queryValue) {
combinedParamKeys.push(key)
}
}
}
serverUtils.normalizeCdnUrl(req, combinedParamKeys)
serverUtils.normalizeQueryParams(query, routeParamKeys)
serverUtils.filterInternalQuery(originalQuery, combinedParamKeys)
if (pageIsDynamic) {
const queryResult = serverUtils.normalizeDynamicRouteParams(query, true)
const paramsResult = serverUtils.normalizeDynamicRouteParams(
params || {},
true
)
let paramsToInterpolate: ParsedUrlQuery
if (
// if both query and params are valid but one
// provided more information and the query params
// were nxtP prefixed rely on that one
query &&
params &&
paramsResult.hasValidParams &&
queryResult.hasValidParams &&
routeParamKeys.size > 0 &&
Object.keys(paramsResult.params).length <=
Object.keys(queryResult.params).length
) {
paramsToInterpolate = queryResult.params
params = Object.assign(queryResult.params)
} else {
paramsToInterpolate =
paramsResult.hasValidParams && params
? params
: queryResult.hasValidParams
? query
: {}
}
req.url = serverUtils.interpolateDynamicPath(
req.url || '/',
paramsToInterpolate
)
parsedUrl.pathname = serverUtils.interpolateDynamicPath(
parsedUrl.pathname || '/',
paramsToInterpolate
)
originalPathname = serverUtils.interpolateDynamicPath(
originalPathname,
paramsToInterpolate
)
// try pulling from query if valid
if (!params) {
if (queryResult.hasValidParams) {
params = Object.assign({}, queryResult.params)
// If we pulled from query remove it so it's
// only in params
for (const key in serverUtils.defaultRouteMatches) {
delete query[key]
}
} else {
// use final params from URL matching
const paramsMatch = serverUtils.dynamicRouteMatcher?.(
normalizeDataPath(
localeResult?.pathname || parsedUrl.pathname || '/'
)
)
// we don't normalize these as they are allowed to be
// the literal slug matches here e.g. /blog/[slug]
// actually being requested
if (paramsMatch) {
params = Object.assign({}, paramsMatch)
}
}
}
// When partial nxtP* params are provided (e.g. background
// revalidation for intermediate PPR shells), both
// normalizeDynamicRouteParams calls above fail because not all
// route params are present. Merge the normalized query params
// (from nxtP*) into the current params to override placeholders
// with concrete values.
if (
params &&
routeParamKeys.size > 0 &&
!paramsResult.hasValidParams &&
!queryResult.hasValidParams
) {
for (const key of routeParamKeys) {
if (query[key] !== undefined) {
params[key] = query[key]
}
}
addRequestMeta(req, 'resolvedRouteParamKeys', routeParamKeys)
}
}
// Remove any normalized params from the query if they
// weren't present as non-prefixed query key e.g.
// ?search=1&nxtPsearch=hello we don't delete search
for (const key of routeParamKeys) {
if (!(key in originalQuery)) {
delete query[key]
// handle the case where there's collision and we
// normalized nxtPid=123 -> id=123 but user also
// sends id=456 as separate key
} else if (
originalQuery[key] &&
query[key] &&
originalQuery[key] !== query[key]
) {
query[key] = originalQuery[key]
}
}
const { isOnDemandRevalidate, revalidateOnlyGenerated } =
checkIsOnDemandRevalidate(req, prerenderManifest.preview)
let isDraftMode = false
let previewData: PreviewData
// preview data relies on non-edge utils
if (process.env.NEXT_RUNTIME !== 'edge' && res) {
const { tryGetPreviewData } =
require('../api-utils/node/try-get-preview-data') as typeof import('../api-utils/node/try-get-preview-data')
previewData = tryGetPreviewData(
req,
res,
prerenderManifest.preview,
Boolean(multiZoneDraftMode)
)
isDraftMode = previewData !== false
}
if (!nextConfig) {
throw new Error("Invariant: nextConfig couldn't be loaded")
}
if (process.env.NEXT_RUNTIME !== 'edge') {
const { installProcessErrorHandlers } =
require('../node-environment-extensions/process-error-handlers') as typeof import('../node-environment-extensions/process-error-handlers')
installProcessErrorHandlers(
Boolean(
nextConfig.experimental.removeUncaughtErrorAndRejectionListeners
)
)
}
let resolvedPathname = normalizedSrcPage
if (isDynamicRoute(resolvedPathname) && params) {
resolvedPathname = serverUtils.interpolateDynamicPath(
resolvedPathname,
params
)
}
if (resolvedPathname === '/index') {
resolvedPathname = '/'
}
if (
res &&
Boolean(req.headers['x-nextjs-data']) &&
(!res.statusCode || res.statusCode === 200)
) {
res.setHeader(
'x-nextjs-matched-path',
removeTrailingSlash(`${locale ? `/${locale}` : ''}${normalizedSrcPage}`)
)
}
const encodedResolvedPathname = resolvedPathname
// we decode for cache key/manifest usage encoded is
// for URL building
try {
resolvedPathname = decodePathParams(resolvedPathname)
} catch (_) {}
resolvedPathname = removeTrailingSlash(resolvedPathname)
addRequestMeta(req, 'resolvedPathname', resolvedPathname)
let deploymentId
if (nextConfig.experimental?.runtimeServerDeploymentId) {
if (!process.env.NEXT_DEPLOYMENT_ID) {
throw new Error(
'process.env.NEXT_DEPLOYMENT_ID is missing but runtimeServerDeploymentId is enabled'
)
}
deploymentId = process.env.NEXT_DEPLOYMENT_ID
} else {
deploymentId = nextConfig.deploymentId || ''
}
return {
query,
originalQuery,
originalPathname,
params,
parsedUrl,
locale,
isNextDataRequest,
locales: i18n?.locales,
defaultLocale,
isDraftMode,
previewData,
pageIsDynamic,
resolvedPathname,
encodedResolvedPathname,
isOnDemandRevalidate,
revalidateOnlyGenerated,
...manifests,
// loadManifest returns a readonly object, but we don't want to propagate that throughout the
// whole codebase (for now)
nextConfig:
nextConfig satisfies DeepReadonly<NextConfigRuntime> as NextConfigRuntime,
routerServerContext,
deploymentId,
clientAssetToken: nextConfig.experimental.supportsImmutableAssets
? ''
: deploymentId,
}
}
public getResponseCache(req: IncomingMessage | BaseNextRequest) {
if (!this.responseCache) {
const minimalMode = getRequestMeta(req, 'minimalMode') ?? false
this.responseCache = new ResponseCache(minimalMode)
}
return this.responseCache
}
public async handleResponse({
req,
nextConfig,
cacheKey,
routeKind,
isFallback,
prerenderManifest,
isRoutePPREnabled,
isOnDemandRevalidate,
revalidateOnlyGenerated,
responseGenerator,
waitUntil,
isMinimalMode,
}: {
req: IncomingMessage | BaseNextRequest
nextConfig: NextConfigRuntime
cacheKey: string | null
routeKind: RouteKind
isFallback?: boolean
prerenderManifest: DeepReadonly<PrerenderManifest>
isRoutePPREnabled?: boolean
isOnDemandRevalidate?: boolean
revalidateOnlyGenerated?: boolean
responseGenerator: ResponseGenerator
waitUntil?: (prom: Promise<any>) => void
isMinimalMode: boolean
}) {
const responseCache = this.getResponseCache(req)
const cacheEntry = await responseCache.get(cacheKey, responseGenerator, {
routeKind,
isFallback,
isRoutePPREnabled,
isOnDemandRevalidate,
isPrefetch: req.headers.purpose === 'prefetch',
// Use x-invocation-id header to scope the in-memory cache to a single
// revalidation request in minimal mode.
invocationID: req.headers['x-invocation-id'] as string | undefined,
incrementalCache: await this.getIncrementalCache(
req,
nextConfig,
prerenderManifest,
isMinimalMode
),
waitUntil,
})
if (!cacheEntry) {
if (
cacheKey &&
// revalidate only generated can bail even if cacheKey is provided
!(isOnDemandRevalidate && revalidateOnlyGenerated)
) {
// A cache entry might not be generated if a response is written
// in `getInitialProps` or `getServerSideProps`, but those shouldn't
// have a cache key. If we do have a cache key but we don't end up
// with a cache entry, then either Next.js or the application has a
// bug that needs fixing.
throw new Error('invariant: cache entry required but not generated')
}
}
return cacheEntry
}
}