import type { LoaderTree } from '../../server/lib/app-dir-module'
import type { IncomingMessage, ServerResponse } from 'node:http'
import type { FallbackRouteParam } from '../static-paths/types'
import {
AppPageRouteModule,
type AppPageRouteHandlerContext,
} from '../../server/route-modules/app-page/module.compiled' with { 'turbopack-transition': 'next-ssr' }
import { RouteKind } from '../../server/route-kind' with { 'turbopack-transition': 'next-server-utility' }
import { getRevalidateReason } from '../../server/instrumentation/utils' with { 'turbopack-transition': 'next-server-utility' }
import {
getTracer,
SpanKind,
type Span,
} from '../../server/lib/trace/tracer' with { 'turbopack-transition': 'next-server-utility' }
import type { RequestMeta } from '../../server/request-meta'
import {
addRequestMeta,
getRequestMeta,
setRequestMeta,
} from '../../server/request-meta' with { 'turbopack-transition': 'next-server-utility' }
import { BaseServerSpan } from '../../server/lib/trace/constants' with { 'turbopack-transition': 'next-server-utility' }
import { interopDefault } from '../../server/app-render/interop-default' with { 'turbopack-transition': 'next-server-utility' }
import { stripFlightHeaders } from '../../server/app-render/strip-flight-headers' with { 'turbopack-transition': 'next-server-utility' }
import {
NodeNextRequest,
NodeNextResponse,
} from '../../server/base-http/node' with { 'turbopack-transition': 'next-server-utility' }
import { checkIsAppPPREnabled } from '../../server/lib/experimental/ppr' with { 'turbopack-transition': 'next-server-utility' }
import {
getFallbackRouteParams,
getPlaceholderFallbackRouteParams,
buildDynamicSegmentPlaceholder,
createOpaqueFallbackRouteParams,
type OpaqueFallbackRouteParams,
} from '../../server/request/fallback-params' with { 'turbopack-transition': 'next-server-utility' }
import { setManifestsSingleton } from '../../server/app-render/manifests-singleton' with { 'turbopack-transition': 'next-server-utility' }
import {
isHtmlBotRequest,
shouldServeStreamingMetadata,
} from '../../server/lib/streaming-metadata' with { 'turbopack-transition': 'next-server-utility' }
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths' with { 'turbopack-transition': 'next-server-utility' }
import { getIsPossibleServerAction } from '../../server/lib/server-action-request-meta' with { 'turbopack-transition': 'next-server-utility' }
import {
RSC_HEADER,
NEXT_ROUTER_PREFETCH_HEADER,
NEXT_ROUTER_SEGMENT_PREFETCH_HEADER,
NEXT_INSTANT_PREFETCH_HEADER,
NEXT_INSTANT_TEST_COOKIE,
NEXT_IS_PRERENDER_HEADER,
NEXT_DID_POSTPONE_HEADER,
RSC_CONTENT_TYPE_HEADER,
} from '../../client/components/app-router-headers' with { 'turbopack-transition': 'next-server-utility' }
import {
getBotType,
isBot,
} from '../../shared/lib/router/utils/is-bot' with { 'turbopack-transition': 'next-server-utility' }
import {
CachedRouteKind,
IncrementalCacheKind,
type CachedAppPageValue,
type CachedPageValue,
type ResponseCacheEntry,
type ResponseGenerator,
} from '../../server/response-cache' with { 'turbopack-transition': 'next-server-utility' }
import {
FallbackMode,
parseFallbackField,
} from '../../lib/fallback' with { 'turbopack-transition': 'next-server-utility' }
import RenderResult from '../../server/render-result' with { 'turbopack-transition': 'next-server-utility' }
import {
CACHE_ONE_YEAR_SECONDS,
HTML_CONTENT_TYPE_HEADER,
NEXT_CACHE_TAGS_HEADER,
NEXT_NAV_DEPLOYMENT_ID_HEADER,
NEXT_RESUME_HEADER,
NEXT_RESUME_STATE_LENGTH_HEADER,
} from '../../lib/constants' with { 'turbopack-transition': 'next-server-utility' }
import type { CacheControl } from '../../server/lib/cache-control'
import { ENCODED_TAGS } from '../../server/stream-utils/encoded-tags' with { 'turbopack-transition': 'next-server-utility' }
import { createInstantTestScriptInsertionTransformStream } from '../../server/stream-utils/node-web-streams-helper' with { 'turbopack-transition': 'next-server-utility' }
import { sendRenderResult } from '../../server/send-payload' with { 'turbopack-transition': 'next-server-utility' }
import { NoFallbackError } from '../../shared/lib/no-fallback-error.external' with { 'turbopack-transition': 'next-server-utility' }
import { parseMaxPostponedStateSize } from '../../shared/lib/size-limit' with { 'turbopack-transition': 'next-server-utility' }
import {
getMaxPostponedStateSize,
getPostponedStateExceededErrorMessage,
readBodyWithSizeLimit,
} from '../../server/lib/postponed-request-body' with { 'turbopack-transition': 'next-server-utility' }
import { parseUrl } from '../../lib/url' with { 'turbopack-transition': 'next-server-utility' }
// These are injected by the loader afterwards.
/**
* The tree created in next-app-loader that holds component segments and modules
* and I've updated it.
*/
declare const tree: LoaderTree
// These are injected by the loader afterwards.
declare const __next_app_require__: (id: string | number) => unknown
declare const __next_app_load_chunk__: (id: string | number) => Promise<unknown>
// We inject the tree and pages here so that we can use them in the route
// module.
// INJECT:tree
// INJECT:__next_app_require__
// INJECT:__next_app_load_chunk__
export const __next_app__ = {
require: __next_app_require__,
loadChunk: __next_app_load_chunk__,
}
import * as entryBase from '../../server/app-render/entry-base' with { 'turbopack-transition': 'next-server-utility' }
import { RedirectStatusCode } from '../../client/components/redirect-status-code' with { 'turbopack-transition': 'next-server-utility' }
import { InvariantError } from '../../shared/lib/invariant-error' with { 'turbopack-transition': 'next-server-utility' }
import { scheduleOnNextTick } from '../../lib/scheduler' with { 'turbopack-transition': 'next-server-utility' }
import { isInterceptionRouteAppPath } from '../../shared/lib/router/utils/interception-routes' with { 'turbopack-transition': 'next-server-utility' }
import { getSegmentParam } from '../../shared/lib/router/utils/get-segment-param' with { 'turbopack-transition': 'next-server-utility' }
export * from '../../server/app-render/entry-base' with { 'turbopack-transition': 'next-server-utility' }
// Create and export the route module that will be consumed.
export const routeModule = new AppPageRouteModule({
definition: {
kind: RouteKind.APP_PAGE,
page: 'VAR_DEFINITION_PAGE',
pathname: 'VAR_DEFINITION_PATHNAME',
// The following aren't used in production.
bundlePath: '',
filename: '',
appPaths: [],
},
userland: {
loaderTree: tree,
},
distDir: process.env.__NEXT_RELATIVE_DIST_DIR || '',
relativeProjectDir: process.env.__NEXT_RELATIVE_PROJECT_DIR || '',
})
/**
* Builds the cache key for the most complete prerenderable shell we can derive
* from the shell that matched this request. Only params that can still be
* filled by `generateStaticParams` are substituted; fully dynamic params stay
* as placeholders so a request like `/c/foo` can complete `/[one]/[two]` into
* `/c/[two]` rather than `/c/foo`.
*/
function buildCompletedShellCacheKey(
fallbackPathname: string,
remainingPrerenderableParams: readonly FallbackRouteParam[],
params: Record<string, undefined | string | string[]> | undefined
): string {
const prerenderableParamsByName = new Map(
remainingPrerenderableParams.map((param) => [param.paramName, param])
)
return (
fallbackPathname
.split('/')
.map((segment) => {
const segmentParam = getSegmentParam(segment)
if (!segmentParam) {
return segment
}
const remainingParam = prerenderableParamsByName.get(
segmentParam.paramName
)
if (!remainingParam) {
return segment
}
const value = params?.[remainingParam.paramName]
if (!value) {
return segment
}
const encodedValue = Array.isArray(value)
? value.map((item) => encodeURIComponent(item)).join('/')
: encodeURIComponent(value)
return segment.replace(
buildDynamicSegmentPlaceholder(remainingParam),
encodedValue
)
})
.join('/') || '/'
)
}
export async function handler(
req: IncomingMessage,
res: ServerResponse,
ctx: {
waitUntil?: (prom: Promise<void>) => void
requestMeta?: RequestMeta
}
) {
if (ctx.requestMeta) {
setRequestMeta(req, ctx.requestMeta)
}
if (routeModule.isDev) {
addRequestMeta(req, 'devRequestTimingInternalsEnd', process.hrtime.bigint())
}
const isMinimalMode = Boolean(getRequestMeta(req, 'minimalMode'))
let srcPage = 'VAR_DEFINITION_PAGE'
// turbopack doesn't normalize `/index` in the page name
// so we need to to process dynamic routes properly
// TODO: fix turbopack providing differing value from webpack
if (process.env.TURBOPACK) {
srcPage = srcPage.replace(/\/index$/, '') || '/'
} else if (srcPage === '/index') {
// we always normalize /index specifically
srcPage = '/'
}
const multiZoneDraftMode = process.env
.__NEXT_MULTI_ZONE_DRAFT_MODE as any as boolean
const prepareResult = await routeModule.prepare(req, res, {
srcPage,
multiZoneDraftMode,
})
if (!prepareResult) {
res.statusCode = 400
res.end('Bad Request')
ctx.waitUntil?.(Promise.resolve())
return null
}
const {
buildId,
query,
params,
pageIsDynamic,
buildManifest,
nextFontManifest,
reactLoadableManifest,
serverActionsManifest,
clientReferenceManifest,
subresourceIntegrityManifest,
prerenderManifest,
prefetchHintsManifest,
isDraftMode,
resolvedPathname,
revalidateOnlyGenerated,
routerServerContext,
nextConfig,
parsedUrl,
interceptionRoutePatterns,
deploymentId,
clientAssetToken,
} = prepareResult
const normalizedSrcPage = normalizeAppPath(srcPage)
let { isOnDemandRevalidate } = prepareResult
// We use the resolvedPathname instead of the parsedUrl.pathname because it
// is not rewritten as resolvedPathname is. This will ensure that the correct
// prerender info is used instead of using the original pathname as the
// source. If however PPR is enabled and cacheComponents is disabled, we
// treat the pathname as dynamic. Currently, there's a bug in the PPR
// implementation that incorrectly leaves %%drp placeholders in the output of
// parallel routes. This is addressed with cacheComponents.
const prerenderMatch =
nextConfig.experimental.ppr &&
!nextConfig.cacheComponents &&
isInterceptionRouteAppPath(resolvedPathname)
? null
: routeModule.match(resolvedPathname, prerenderManifest)
const prerenderInfo = prerenderMatch?.route ?? null
const isPrerendered = !!prerenderManifest.routes[resolvedPathname]
const userAgent = req.headers['user-agent'] || ''
const botType = getBotType(userAgent)
const isHtmlBot = isHtmlBotRequest(req)
/**
* If true, this indicates that the request being made is for an app
* prefetch request.
*/
const isPrefetchRSCRequest =
getRequestMeta(req, 'isPrefetchRSCRequest') ??
req.headers[NEXT_ROUTER_PREFETCH_HEADER] === '1' // exclude runtime prefetches, which use '2'
// NOTE: Don't delete headers[RSC] yet, it still needs to be used in renderToHTML later
const isRSCRequest =
getRequestMeta(req, 'isRSCRequest') ?? Boolean(req.headers[RSC_HEADER])
const isPossibleServerAction = getIsPossibleServerAction(req)
/**
* If the route being rendered is an app page, and the ppr feature has been
* enabled, then the given route _could_ support PPR.
*/
const couldSupportPPR: boolean = checkIsAppPPREnabled(
nextConfig.experimental.ppr
)
// Stash postponed state for server actions when in minimal mode.
// We extract it here so the RDC is available for the re-render after the action completes.
const resumeStateLengthHeader = req.headers[NEXT_RESUME_STATE_LENGTH_HEADER]
if (
!getRequestMeta(req, 'postponed') &&
isMinimalMode &&
couldSupportPPR &&
isPossibleServerAction &&
resumeStateLengthHeader &&
typeof resumeStateLengthHeader === 'string'
) {
const stateLength = parseInt(resumeStateLengthHeader, 10)
const { maxPostponedStateSize, maxPostponedStateSizeBytes } =
getMaxPostponedStateSize(nextConfig.experimental.maxPostponedStateSize)
if (!isNaN(stateLength) && stateLength > 0) {
if (stateLength > maxPostponedStateSizeBytes) {
res.statusCode = 413
res.end(getPostponedStateExceededErrorMessage(maxPostponedStateSize))
ctx.waitUntil?.(Promise.resolve())
return null
}
// Calculate max total body size to prevent buffering excessively large
// payloads before the action handler checks. We use stateLength (not
// maxPostponedStateSizeBytes) so the postponed state doesn't eat into
// the action body budget - it's already validated above.
const defaultActionBodySizeLimit = '1 MB'
const actionBodySizeLimit =
nextConfig.experimental.serverActions?.bodySizeLimit ??
defaultActionBodySizeLimit
const actionBodySizeLimitBytes =
actionBodySizeLimit !== defaultActionBodySizeLimit
? (
require('next/dist/compiled/bytes') as typeof import('next/dist/compiled/bytes')
).parse(actionBodySizeLimit)
: 1024 * 1024 // 1 MB
const maxTotalBodySize = stateLength + actionBodySizeLimitBytes
const fullBody = await readBodyWithSizeLimit(req, maxTotalBodySize)
if (fullBody === null) {
res.statusCode = 413
res.end(
`Request body exceeded limit. ` +
`To configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/api-reference/next-config-js/serverActions#bodysizelimit`
)
ctx.waitUntil?.(Promise.resolve())
return null
}
if (fullBody.length >= stateLength) {
// Extract postponed state from the beginning
const postponedState = fullBody
.subarray(0, stateLength)
.toString('utf8')
addRequestMeta(req, 'postponed', postponedState)
// Store the remaining action body for the action handler
const actionBody = fullBody.subarray(stateLength)
addRequestMeta(req, 'actionBody', actionBody)
} else {
throw new Error(
`invariant: expected ${stateLength} bytes of postponed state but only received ${fullBody.length} bytes`
)
}
}
}
if (
!getRequestMeta(req, 'postponed') &&
couldSupportPPR &&
req.headers[NEXT_RESUME_HEADER] === '1' &&
req.method === 'POST'
) {
const { maxPostponedStateSize, maxPostponedStateSizeBytes } =
getMaxPostponedStateSize(nextConfig.experimental.maxPostponedStateSize)
// Decode the postponed state from the request body, it will come as
// an array of buffers, so collect them and then concat them to form
// the string.
const body = await readBodyWithSizeLimit(req, maxPostponedStateSizeBytes)
if (body === null) {
res.statusCode = 413
res.end(getPostponedStateExceededErrorMessage(maxPostponedStateSize))
ctx.waitUntil?.(Promise.resolve())
return null
}
const postponed = body.toString('utf8')
addRequestMeta(req, 'postponed', postponed)
}
// When enabled, this will allow the use of the `?__nextppronly` query to
// enable debugging of the static shell.
const hasDebugStaticShellQuery =
process.env.__NEXT_EXPERIMENTAL_STATIC_SHELL_DEBUGGING === '1' &&
typeof query.__nextppronly !== 'undefined' &&
couldSupportPPR
// When enabled, this will allow the use of the `?__nextppronly` query
// to enable debugging of the fallback shell.
const hasDebugFallbackShellQuery =
hasDebugStaticShellQuery && query.__nextppronly === 'fallback'
// Whether the testing API is exposed (dev mode or explicit flag)
const exposeTestingApi =
routeModule.isDev === true ||
nextConfig.experimental.exposeTestingApiInProductionBuild === true
// Enable the Instant Navigation Testing API. Renders only the prefetched
// portion of the page, excluding dynamic content. This allows tests to
// assert on the prefetched UI state deterministically.
// - Header: Used for client-side navigations where we can set request headers
// - Cookie: Used for MPA navigations (page reload, full page load) where we
// can't set request headers. Only applies to document requests (no RSC
// header) - RSC requests should proceed normally even during a locked scope,
// with blocking happening on the client side.
const isInstantNavigationTest =
exposeTestingApi &&
(req.headers[NEXT_INSTANT_PREFETCH_HEADER] === '1' ||
(req.headers[RSC_HEADER] === undefined &&
typeof req.headers.cookie === 'string' &&
req.headers.cookie.includes(NEXT_INSTANT_TEST_COOKIE + '=')))
// This page supports PPR if it is marked as being `PARTIALLY_STATIC` in the
// prerender manifest and this is an app page.
const isRoutePPREnabled: boolean =
// When the instant navigation testing API is active, enable the PPR
// prerender path even without Cache Components. In dev mode without CC,
// static pages need this path to produce buffered segment data (the
// legacy prerender path hangs in dev mode).
(couldSupportPPR || isInstantNavigationTest) &&
((
prerenderManifest.routes[normalizedSrcPage] ??
prerenderManifest.dynamicRoutes[normalizedSrcPage]
)?.renderingMode === 'PARTIALLY_STATIC' ||
// Ideally we'd want to check the appConfig to see if this page has PPR
// enabled or not, but that would require plumbing the appConfig through
// to the server during development. We assume that the page supports it
// but only during development or when the testing API is exposed.
((hasDebugStaticShellQuery || isInstantNavigationTest) &&
(exposeTestingApi ||
routerServerContext?.experimentalTestProxy === true)))
const isDebugStaticShell: boolean =
(hasDebugStaticShellQuery || isInstantNavigationTest) && isRoutePPREnabled
// We should enable debugging dynamic accesses when the static shell
// debugging has been enabled and we're also in development mode.
const isDebugDynamicAccesses =
isDebugStaticShell && routeModule.isDev === true
const isDebugFallbackShell = hasDebugFallbackShellQuery && isRoutePPREnabled
// If we're in minimal mode, then try to get the postponed information from
// the request metadata. If available, use it for resuming the postponed
// render.
const minimalPostponed = isRoutePPREnabled
? getRequestMeta(req, 'postponed')
: undefined
// If PPR is enabled, and this is a RSC request (but not a prefetch), then
// we can use this fact to only generate the flight data for the request
// because we can't cache the HTML (as it's also dynamic).
const staticPrefetchDataRoute =
prerenderManifest.routes[resolvedPathname]?.prefetchDataRoute
let isDynamicRSCRequest =
isRoutePPREnabled &&
isRSCRequest &&
!isPrefetchRSCRequest &&
// If generated at build time, treat the RSC request as static
// so we can serve the prebuilt .rsc without a dynamic render.
// Only do this for routes that have a concrete prefetchDataRoute.
!staticPrefetchDataRoute
// During a PPR revalidation, the RSC request is not dynamic if we do not have the postponed data.
// We only attach the postponed data during a resume. If there's no postponed data, then it must be a revalidation.
// This is to ensure that we don't bypass the cache during a revalidation.
if (isMinimalMode) {
isDynamicRSCRequest = isDynamicRSCRequest && !!minimalPostponed
}
// Need to read this before it's stripped by stripFlightHeaders. We don't
// need to transfer it to the request meta because it's only read
// within this function; the static segment data should have already been
// generated, so we will always either return a static response or a 404.
const rawSegmentPrefetchHeader =
req.headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]
const segmentPrefetchHeader =
getRequestMeta(req, 'segmentPrefetchRSCRequest') ??
(isPrefetchRSCRequest
? typeof rawSegmentPrefetchHeader === 'string'
? rawSegmentPrefetchHeader
: Array.isArray(rawSegmentPrefetchHeader)
? rawSegmentPrefetchHeader[0]
: undefined
: undefined)
// TODO: investigate existing bug with shouldServeStreamingMetadata always
// being true for a revalidate due to modifying the base-server this.renderOpts
// when fixing this to correct logic it causes hydration issue since we set
// serveStreamingMetadata to true during export
const serveStreamingMetadata =
botType && isRoutePPREnabled
? false
: !userAgent
? true
: shouldServeStreamingMetadata(userAgent, nextConfig.htmlLimitedBots)
const isSSG = Boolean(
(prerenderInfo ||
isPrerendered ||
prerenderManifest.routes[normalizedSrcPage]) &&
// If this is a bot request and PPR is enabled, then we don't want
// to serve a static response. This applies to both DOM bots (like Googlebot)
// and HTML-limited bots.
!(botType && isRoutePPREnabled)
)
// When a page supports cacheComponents, we can support RDC for Navigations
const supportsRDCForNavigations =
isRoutePPREnabled && nextConfig.cacheComponents === true
// In development, we always want to generate dynamic HTML.
const supportsDynamicResponse: boolean =
// If we're in development, we always support dynamic HTML, unless it's
// a data request, in which case we only produce static HTML.
routeModule.isDev === true ||
// If this is a draft mode request, it supports dynamic HTML.
isDraftMode ||
// If this is not SSG or does not have static paths, then it supports
// dynamic HTML.
!isSSG ||
// If this request has provided postponed data, it supports dynamic
// HTML.
typeof minimalPostponed === 'string' ||
// If this handler supports onCacheEntryV2, then we can only support
// dynamic responses if it's a dynamic RSC request and not in minimal mode. If it
// doesn't support it we must fallback to the default behavior.
(supportsRDCForNavigations && getRequestMeta(req, 'onCacheEntryV2')
? // In minimal mode, we'll always want to generate a static response
// which will generate the RDC for the route. When resuming a Dynamic
// RSC request, we'll pass the minimal postponed data to the render
// which will trigger the `supportsDynamicResponse` to be true.
isDynamicRSCRequest && !isMinimalMode
: // Otherwise, we can support dynamic responses if it's a dynamic RSC request.
isDynamicRSCRequest)
// When bots request PPR page, perform the full dynamic rendering.
// This applies to both DOM bots (like Googlebot) and HTML-limited bots.
const shouldWaitOnAllReady = Boolean(botType) && isRoutePPREnabled
const remainingPrerenderableParams =
prerenderInfo?.remainingPrerenderableParams ?? []
// Concrete optional routes like `/optional-catchall` can still match their
// generic shell entry (eg /optional-catchall/[[...slug]]) in the prerender manifest.
// If the omitted param already resolved to a real prerendered path, keep serving that concrete result.
const hasOmittedConcreteFallbackParam =
isPrerendered &&
remainingPrerenderableParams.some((param) => {
const value = params?.[param.paramName]
return value == null || (Array.isArray(value) && value.length === 0)
})
const hasUnresolvedRootFallbackParams =
prerenderInfo?.fallback === null &&
(prerenderInfo.fallbackRootParams?.length ?? 0) > 0
let ssgCacheKey: string | null = null
if (
!isDraftMode &&
isSSG &&
!supportsDynamicResponse &&
!isPossibleServerAction &&
!minimalPostponed &&
!isDynamicRSCRequest
) {
// For normal SSG routes we cache by the fully resolved pathname. For
// partial fallbacks we instead derive the cache key from the shell
// that matched this request so `/prefix/[one]/[two]` can specialize into
// `/prefix/c/[two]` without promoting all the way to `/prefix/c/foo`.
const fallbackPathname = prerenderMatch
? typeof prerenderInfo?.fallback === 'string'
? prerenderInfo.fallback
: prerenderMatch.source
: null
if (
nextConfig.experimental.partialFallbacks === true &&
fallbackPathname &&
prerenderInfo?.fallbackRouteParams?.length &&
!hasUnresolvedRootFallbackParams
) {
if (remainingPrerenderableParams.length > 0) {
const completedShellCacheKey = buildCompletedShellCacheKey(
fallbackPathname,
remainingPrerenderableParams,
params
)
// If applying the current request params doesn't make the shell any
// more complete, then this shell is already at its most complete
// form and should remain shared rather than creating a new cache entry.
ssgCacheKey =
completedShellCacheKey !== fallbackPathname
? completedShellCacheKey
: null
}
} else {
ssgCacheKey = resolvedPathname
}
}
// the staticPathKey differs from ssgCacheKey since
// ssgCacheKey is null in dev since we're always in "dynamic"
// mode in dev to bypass the cache. It can also be null for partial
// fallback shells that should remain shared and must not create a
// param-specific ISR entry, but we still need to honor fallback handling.
let staticPathKey = ssgCacheKey
if (
!staticPathKey &&
(routeModule.isDev ||
(isSSG &&
pageIsDynamic &&
prerenderInfo?.fallbackRouteParams?.length &&
// Server action requests must not get a staticPathKey, otherwise they
// enter the fallback rendering block below and return the cached HTML
// shell with the action result appended, instead of responding with
// just the RSC action result.
!isPossibleServerAction))
) {
staticPathKey = resolvedPathname
}
// If this is a request for an app path that should be statically generated
// and we aren't in the edge runtime, strip the flight headers so it will
// generate the static response.
if (
!routeModule.isDev &&
!isDraftMode &&
isSSG &&
isRSCRequest &&
!isDynamicRSCRequest
) {
stripFlightHeaders(req.headers)
}
const ComponentMod = {
...entryBase,
tree,
handler,
routeModule,
__next_app__,
}
// Before rendering (which initializes component tree modules), we have to
// set the reference manifests to our global store so Server Action's
// encryption util can access to them at the top level of the page module.
if (serverActionsManifest && clientReferenceManifest) {
setManifestsSingleton({
page: srcPage,
clientReferenceManifest,
serverActionsManifest,
})
}
const method = req.method || 'GET'
const tracer = getTracer()
const activeSpan = tracer.getActiveScopeSpan()
const isWrappedByNextServer = Boolean(
routerServerContext?.isWrappedByNextServer
)
const remainingFallbackRouteParams =
nextConfig.experimental.partialFallbacks === true &&
remainingPrerenderableParams.length > 0
? (prerenderInfo?.fallbackRouteParams?.filter(
(param) =>
!remainingPrerenderableParams.some(
(prerenderableParam) =>
prerenderableParam.paramName === param.paramName
)
) ?? [])
: []
const render404 = async () => {
// TODO: should route-module itself handle rendering the 404
if (routerServerContext?.render404) {
await routerServerContext.render404(req, res, parsedUrl, false)
} else {
res.end('This page could not be found')
}
return null
}
try {
const varyHeader = routeModule.getVaryHeader(
resolvedPathname,
interceptionRoutePatterns
)
res.setHeader('Vary', varyHeader)
let parentSpan: Span | undefined
const invokeRouteModule = async (
span: Span | undefined,
context: AppPageRouteHandlerContext
) => {
const nextReq = new NodeNextRequest(req)
const nextRes = new NodeNextResponse(res)
return routeModule.render(nextReq, nextRes, context).finally(() => {
if (!span) return
span.setAttributes({
'http.status_code': res.statusCode,
'next.rsc': false,
})
const rootSpanAttributes = tracer.getRootSpanAttributes()
// We were unable to get attributes, probably OTEL is not enabled
if (!rootSpanAttributes) {
return
}
if (
rootSpanAttributes.get('next.span_type') !==
BaseServerSpan.handleRequest
) {
console.warn(
`Unexpected root span type '${rootSpanAttributes.get(
'next.span_type'
)}'. Please report this Next.js issue https://github.com/vercel/next.js`
)
return
}
const route = rootSpanAttributes.get('next.route') || normalizedSrcPage
const name = `${method} ${route}`
span.setAttributes({
'next.route': route,
'http.route': route,
'next.span_name': name,
})
span.updateName(name)
// Propagate http.route to the parent span if one exists (e.g.
// a platform-created HTTP span in adapter deployments).
if (parentSpan && parentSpan !== span) {
parentSpan.setAttribute('http.route', route)
parentSpan.updateName(name)
}
})
}
const incrementalCache =
getRequestMeta(req, 'incrementalCache') ||
(await routeModule.getIncrementalCache(
req,
nextConfig,
prerenderManifest,
isMinimalMode
))
incrementalCache?.resetRequestCache()
;(globalThis as any).__incrementalCache = incrementalCache
const doRender = async ({
span,
postponed,
fallbackRouteParams,
forceStaticRender,
}: {
span?: Span
/**
* The postponed data for this render. This is only provided when resuming
* a render that has been postponed.
*/
postponed: string | undefined
/**
* The unknown route params for this render.
*/
fallbackRouteParams: OpaqueFallbackRouteParams | null
/**
* When true, this indicates that the response generator is being called
* in a context where the response must be generated statically.
*
* CRITICAL: This should only currently be used when revalidating due to a
* dynamic RSC request.
*/
forceStaticRender: boolean
}): Promise<ResponseCacheEntry> => {
const context: AppPageRouteHandlerContext = {
query,
params,
page: normalizedSrcPage,
sharedContext: {
buildId,
deploymentId,
clientAssetToken,
},
serverComponentsHmrCache: getRequestMeta(
req,
'serverComponentsHmrCache'
),
fallbackRouteParams,
renderOpts: {
App: () => null,
Document: () => null,
pageConfig: {},
ComponentMod,
Component: interopDefault(ComponentMod),
params,
routeModule,
page: srcPage,
postponed,
shouldWaitOnAllReady,
serveStreamingMetadata,
supportsDynamicResponse:
typeof postponed === 'string' || supportsDynamicResponse,
buildManifest,
nextFontManifest,
reactLoadableManifest,
subresourceIntegrityManifest,
setCacheStatus: routerServerContext?.setCacheStatus,
setIsrStatus: routerServerContext?.setIsrStatus,
setReactDebugChannel: routerServerContext?.setReactDebugChannel,
sendErrorsToBrowser: routerServerContext?.sendErrorsToBrowser,
dir:
process.env.NEXT_RUNTIME === 'nodejs'
? (require('path') as typeof import('path')).join(
/* turbopackIgnore: true */
process.cwd(),
routeModule.relativeProjectDir
)
: `${process.cwd()}/${routeModule.relativeProjectDir}`,
isDraftMode,
botType,
isOnDemandRevalidate,
isPossibleServerAction,
assetPrefix: nextConfig.assetPrefix,
nextConfigOutput: nextConfig.output,
crossOrigin: nextConfig.crossOrigin,
trailingSlash: nextConfig.trailingSlash,
images: nextConfig.images,
previewProps: prerenderManifest.preview,
enableTainting: nextConfig.experimental.taint,
htmlLimitedBots: nextConfig.htmlLimitedBots,
reactMaxHeadersLength: nextConfig.reactMaxHeadersLength,
multiZoneDraftMode,
prefetchHints: prefetchHintsManifest,
incrementalCache,
cacheLifeProfiles: nextConfig.cacheLife,
staticPageGenerationTimeout: nextConfig.staticPageGenerationTimeout,
basePath: nextConfig.basePath,
serverActions: nextConfig.experimental.serverActions,
logServerFunctions:
typeof nextConfig.logging === 'object' &&
Boolean(nextConfig.logging.serverFunctions),
...(isDebugStaticShell ||
isDebugDynamicAccesses ||
isDebugFallbackShell
? {
isBuildTimePrerendering: true,
supportsDynamicResponse: false,
isStaticGeneration: true,
isDebugDynamicAccesses: isDebugDynamicAccesses,
}
: {}),
cacheComponents: Boolean(nextConfig.cacheComponents),
experimental: {
isRoutePPREnabled,
expireTime: nextConfig.expireTime,
staleTimes: nextConfig.experimental.staleTimes,
dynamicOnHover: Boolean(nextConfig.experimental.dynamicOnHover),
optimisticRouting: Boolean(
nextConfig.experimental.optimisticRouting
),
inlineCss: Boolean(nextConfig.experimental.inlineCss),
prefetchInlining: nextConfig.experimental.prefetchInlining ?? false,
authInterrupts: Boolean(nextConfig.experimental.authInterrupts),
useCacheTimeout: nextConfig.experimental.useCacheTimeout,
cachedNavigations: Boolean(
nextConfig.experimental.cachedNavigations
),
clientTraceMetadata:
nextConfig.experimental.clientTraceMetadata || ([] as any),
clientParamParsingOrigins:
nextConfig.experimental.clientParamParsingOrigins,
maxPostponedStateSizeBytes: parseMaxPostponedStateSize(
nextConfig.experimental.maxPostponedStateSize
),
},
waitUntil: ctx.waitUntil,
onClose: (cb) => {
res.on('close', cb)
},
onAfterTaskError: () => {},
onInstrumentationRequestError: (
error,
_request,
errorContext,
silenceLog
) =>
routeModule.onRequestError(
req,
error,
errorContext,
silenceLog,
routerServerContext
),
err: getRequestMeta(req, 'invokeError'),
},
}
// When we're revalidating in the background, we should not allow dynamic
// responses.
if (forceStaticRender) {
context.renderOpts.supportsDynamicResponse = false
}
const result = await invokeRouteModule(span, context)
const { metadata } = result
const {
cacheControl,
headers = {},
// Add any fetch tags that were on the page to the response headers.
fetchTags: cacheTags,
fetchMetrics,
} = metadata
// Apply the `expireTime` fallback as soon as we have the render's
// `cacheControl`, so every downstream consumer (the cache stored via
// `incrementalCache.set`, the response Cache-Control header, the outgoing
// entry returned to `handleResponse`) sees a finalized `cacheControl`
// with a populated `expire`. This mirrors the build-time fallback in
// `build/index.ts` so we don't apply an expire to routes that opt out of
// revalidation entirely (`revalidate: false`) or that are dynamic
// (`revalidate: 0`).
if (
cacheControl &&
cacheControl.revalidate !== false &&
cacheControl.revalidate > 0 &&
cacheControl.expire === undefined
) {
cacheControl.expire = nextConfig.expireTime
}
if (cacheTags) {
headers[NEXT_CACHE_TAGS_HEADER] = cacheTags
}
// Pull any fetch metrics from the render onto the request.
;(req as any).fetchMetrics = fetchMetrics
// we don't throw static to dynamic errors in dev as isSSG
// is a best guess in dev since we don't have the prerender pass
// to know whether the path is actually static or not
if (
isSSG &&
cacheControl?.revalidate === 0 &&
!routeModule.isDev &&
!isRoutePPREnabled
) {
const staticBailoutInfo = metadata.staticBailoutInfo
const err = new Error(
`Page changed from static to dynamic at runtime ${resolvedPathname}${
staticBailoutInfo?.description
? `, reason: ${staticBailoutInfo.description}`
: ``
}` +
`\nsee more here https://nextjs.org/docs/messages/app-static-to-dynamic-error`
)
if (staticBailoutInfo?.stack) {
const stack = staticBailoutInfo.stack
err.stack = err.message + stack.substring(stack.indexOf('\n'))
}
throw err
}
return {
value: {
kind: CachedRouteKind.APP_PAGE,
html: result,
headers,
rscData: metadata.flightData,
postponed: metadata.postponed,
status: metadata.statusCode,
segmentData: metadata.segmentData,
} satisfies CachedAppPageValue,
cacheControl,
} satisfies ResponseCacheEntry
}
const responseGenerator: ResponseGenerator = async ({
hasResolved,
previousCacheEntry: previousIncrementalCacheEntry,
isRevalidating,
span,
forceStaticRender = false,
}) => {
const isProduction = routeModule.isDev === false
const didRespond = hasResolved || res.writableEnded
try {
// skip on-demand revalidate if cache is not present and
// revalidate-if-generated is set
if (
isOnDemandRevalidate &&
revalidateOnlyGenerated &&
!previousIncrementalCacheEntry &&
!isMinimalMode
) {
if (routerServerContext?.render404) {
await routerServerContext.render404(req, res)
} else {
res.statusCode = 404
res.end('This page could not be found')
}
return null
}
let fallbackMode: FallbackMode | undefined
if (prerenderInfo) {
fallbackMode = parseFallbackField(prerenderInfo.fallback)
}
if (
nextConfig.experimental.partialFallbacks === true &&
prerenderInfo?.fallback === null &&
!hasOmittedConcreteFallbackParam &&
!hasUnresolvedRootFallbackParams &&
remainingPrerenderableParams.length > 0
) {
// Generic source shells without unresolved root params don't have a
// concrete fallback file of their own, so they're marked as blocking.
// When we can complete the shell into a more specific
// prerendered shell for this request, treat it like a prerender
// fallback so we can serve that shell instead of blocking on the full
// route. Root-param shells stay blocking, since unknown root branches
// should not inherit a shell from another generated branch.
fallbackMode = FallbackMode.PRERENDER
}
// When serving a HTML bot request, we want to serve a blocking render and
// not the prerendered page. This ensures that the correct content is served
// to the bot in the head.
if (fallbackMode === FallbackMode.PRERENDER && isBot(userAgent)) {
if (!isRoutePPREnabled || isHtmlBot) {
fallbackMode = FallbackMode.BLOCKING_STATIC_RENDER
}
}
if (previousIncrementalCacheEntry?.isStale === -1) {
isOnDemandRevalidate = true
}
// TODO: adapt for PPR
// only allow on-demand revalidate for fallback: true/blocking
// or for prerendered fallback: false paths
if (
isOnDemandRevalidate &&
(fallbackMode !== FallbackMode.NOT_FOUND ||
previousIncrementalCacheEntry)
) {
fallbackMode = FallbackMode.BLOCKING_STATIC_RENDER
}
if (
!isMinimalMode &&
fallbackMode !== FallbackMode.BLOCKING_STATIC_RENDER &&
staticPathKey &&
!didRespond &&
!isDraftMode &&
pageIsDynamic &&
(isProduction || !isPrerendered)
) {
// if the page has dynamicParams: false and this pathname wasn't
// prerendered trigger the no fallback handling
if (
// In development, fall through to render to handle missing
// getStaticPaths.
(isProduction || prerenderInfo) &&
// When fallback isn't present, abort this render so we 404
fallbackMode === FallbackMode.NOT_FOUND
) {
if (nextConfig.adapterPath) {
return await render404()
}
throw new NoFallbackError()
}
// When cacheComponents is enabled, we can use the fallback
// response if the request is not a dynamic RSC request because the
// RSC data when this feature flag is enabled does not contain any
// param references. Without this feature flag enabled, the RSC data
// contains param references, and therefore we can't use the fallback.
if (
isRoutePPREnabled &&
(nextConfig.cacheComponents ? !isDynamicRSCRequest : !isRSCRequest)
) {
const cacheKey =
isProduction && typeof prerenderInfo?.fallback === 'string'
? prerenderInfo.fallback
: normalizedSrcPage
let fallbackRouteParams: OpaqueFallbackRouteParams | null
if (isProduction) {
// In production, rely on the prerender manifest's fallback
// entry — the authoritative set computed at build time by
// `buildAppStaticPaths`.
if (prerenderInfo?.fallbackRouteParams) {
fallbackRouteParams = createOpaqueFallbackRouteParams(
prerenderInfo.fallbackRouteParams
)
} else if (isDebugFallbackShell) {
fallbackRouteParams = getFallbackRouteParams(
normalizedSrcPage,
routeModule
)
} else {
fallbackRouteParams = null
}
} else {
// In dev, the prerender manifest isn't populated for ad-hoc
// prefetches. The outer `!isPrerendered` guard means every URL
// reaching this block has params not covered by
// `generateStaticParams`, so the worst-case fallback set —
// every dynamic segment from the loader tree — matches what a
// static prerender would use. This keeps the prefetch response
// from baking resolved param values into the shell.
//
// `isDebugStaticShell` covers the `?__nextppronly=1` query and
// the Instant Navigation testing cookie; `isDebugFallbackShell`
// is the explicit fallback-shell debug flow.
if (isDebugStaticShell || isDebugFallbackShell) {
fallbackRouteParams = getFallbackRouteParams(
normalizedSrcPage,
routeModule
)
} else {
fallbackRouteParams = null
}
}
// When rendering a debug static shell, override the fallback
// params on the request so that the staged rendering correctly
// defers params that are not statically known.
if (isDebugStaticShell && fallbackRouteParams) {
addRequestMeta(req, 'fallbackParams', fallbackRouteParams)
}
// We use the response cache here to handle the revalidation and
// management of the fallback shell.
const fallbackResponse = await routeModule.handleResponse({
cacheKey,
req,
nextConfig,
routeKind: RouteKind.APP_PAGE,
isFallback: true,
prerenderManifest,
isRoutePPREnabled,
responseGenerator: async () =>
doRender({
span,
// We pass `undefined` as rendering a fallback isn't resumed
// here.
postponed: undefined,
// Always serve the shell that matched this request
// immediately. If there are still prerenderable params left,
// the background path below will complete the shell into a
// more specific cache entry for later requests.
fallbackRouteParams,
forceStaticRender: true,
}),
waitUntil: ctx.waitUntil,
isMinimalMode,
})
// If the fallback response was set to null, then we should return null.
if (fallbackResponse === null) return null
// Otherwise, if we did get a fallback response, we should return it.
if (fallbackResponse) {
if (
!isMinimalMode &&
isRoutePPREnabled &&
// Match the build-time contract: only fallback shells that can
// still be completed with prerenderable params should upgrade.
remainingPrerenderableParams.length > 0 &&
nextConfig.experimental.partialFallbacks === true &&
ssgCacheKey &&
incrementalCache &&
!isOnDemandRevalidate &&
!isDebugFallbackShell &&
// The testing API relies on deterministic shell behavior, so
// don't upgrade fallback shells in the background when it's
// exposed.
!exposeTestingApi &&
// Instant Navigation Testing API requests intentionally keep
// the route in shell mode; don't upgrade these in background.
!isInstantNavigationTest
) {
scheduleOnNextTick(async () => {
const responseCache = routeModule.getResponseCache(req)
try {
// Only the params that were just specialized should be
// removed from the fallback render. Any remaining fallback
// params stay deferred so the revalidated result is a more
// specific shell (e.g. `/prefix/c/[two]`), not a fully
// concrete route (`/prefix/c/foo`).
await responseCache.revalidate(
ssgCacheKey,
incrementalCache,
isRoutePPREnabled,
false,
(c) => {
return doRender({
span: c.span,
postponed: undefined,
fallbackRouteParams:
remainingFallbackRouteParams.length > 0
? createOpaqueFallbackRouteParams(
remainingFallbackRouteParams
)
: null,
forceStaticRender: true,
})
},
// We don't have a prior entry for this param-specific shell.
null,
hasResolved,
ctx.waitUntil
)
} catch (err) {
console.error(
'Error revalidating the page in the background',
err
)
}
})
}
// Remove the cache control from the response to prevent it from being
// used in the surrounding cache.
delete fallbackResponse.cacheControl
return fallbackResponse
}
}
}
// Only requests that aren't revalidating can be resumed. If we have the
// minimal postponed data, then we should resume the render with it.
let postponed =
!isOnDemandRevalidate && !isRevalidating && minimalPostponed
? minimalPostponed
: undefined
if (
// If this is a dynamic RSC request or a server action request, we should
// use the postponed data from the static render (if available). This
// ensures that we can utilize the resume data cache (RDC) from the static
// render to ensure that the data is consistent between the static and
// dynamic renders (for navigations) or when re-rendering after a server
// action.
// Only enable RDC for Navigations if the feature is enabled.
supportsRDCForNavigations &&
process.env.NEXT_RUNTIME !== 'edge' &&
!isMinimalMode &&
incrementalCache &&
// Include both dynamic RSC requests (navigations) and server actions
(isDynamicRSCRequest || isPossibleServerAction) &&
// We don't typically trigger an on-demand revalidation for dynamic RSC
// requests, as we're typically revalidating the page in the background
// instead. However, if the cache entry is stale, we should trigger a
// background revalidation on dynamic RSC requests. This prevents us
// from entering an infinite loop of revalidations.
!forceStaticRender
) {
const incrementalCacheEntry = await incrementalCache.get(
resolvedPathname,
{
kind: IncrementalCacheKind.APP_PAGE,
isRoutePPREnabled: true,
isFallback: false,
}
)
// If the cache entry is found, we should use the postponed data from
// the cache.
if (
incrementalCacheEntry &&
incrementalCacheEntry.value &&
incrementalCacheEntry.value.kind === CachedRouteKind.APP_PAGE
) {
// CRITICAL: we're assigning the postponed data from the cache entry
// here as we're using the RDC to resume the render.
postponed = incrementalCacheEntry.value.postponed
// If the cache entry is stale, we should trigger a background
// revalidation so that subsequent requests will get a fresh response.
if (
incrementalCacheEntry &&
// We want to trigger this flow if the cache entry is stale and if
// the requested revalidation flow is either foreground or
// background.
(incrementalCacheEntry.isStale === -1 ||
incrementalCacheEntry.isStale === true)
) {
// We want to schedule this on the next tick to ensure that the
// render is not blocked on it.
scheduleOnNextTick(async () => {
const responseCache = routeModule.getResponseCache(req)
try {
await responseCache.revalidate(
resolvedPathname,
incrementalCache,
isRoutePPREnabled,
false,
(c) =>
responseGenerator({
...c,
// CRITICAL: we need to set this to true as we're
// revalidating in the background and typically this dynamic
// RSC request is not treated as static.
forceStaticRender: true,
}),
// CRITICAL: we need to pass null here because passing the
// previous cache entry here (which is stale) will switch on
// isOnDemandRevalidate and break the prerendering.
null,
hasResolved,
ctx.waitUntil
)
} catch (err) {
console.error(
'Error revalidating the page in the background',
err
)
}
})
}
}
}
// When we're in minimal mode, if we're trying to debug the static shell,
// we should just return nothing instead of resuming the dynamic render.
if (
(isDebugStaticShell || isDebugDynamicAccesses) &&
typeof postponed !== 'undefined'
) {
return {
cacheControl: { revalidate: 1, expire: undefined },
value: {
kind: CachedRouteKind.PAGES,
html: RenderResult.EMPTY,
pageData: {},
headers: undefined,
status: undefined,
} satisfies CachedPageValue,
}
}
const placeholderFallbackRouteParams =
// When a request carries dynamic placeholder values (e.g. "[slug]"),
// defer only the unresolved subset instead of forcing all fallback
// params to suspend.
!routeModule.isDev &&
pageIsDynamic &&
prerenderInfo?.fallbackRouteParams
? getPlaceholderFallbackRouteParams(
params as
| Record<string, undefined | string | string[]>
| undefined,
prerenderInfo.fallbackRouteParams
)
: null
const fallbackRouteParamsForRender =
placeholderFallbackRouteParams &&
placeholderFallbackRouteParams.length > 0
? placeholderFallbackRouteParams
: prerenderInfo?.fallbackRouteParams
const hasPlaceholderFallbackRouteParams =
placeholderFallbackRouteParams != null &&
placeholderFallbackRouteParams.length > 0
// When route-module.ts resolved partial nxtP* params during
// background revalidation, filter fallbackRouteParams to only the
// params that are still unresolved. This lets doRender produce an
// intermediate PPR shell that suspends only for those params.
let effectiveFallbackRouteParams: FallbackRouteParam[] | null = null
if (nextConfig.cacheComponents && prerenderInfo?.fallbackRouteParams) {
const resolvedKeys = getRequestMeta(req, 'resolvedRouteParamKeys')
if (resolvedKeys && resolvedKeys.size > 0) {
effectiveFallbackRouteParams =
prerenderInfo.fallbackRouteParams.filter(
(param) => !resolvedKeys.has(param.paramName)
)
}
}
const fallbackRouteParams =
// In production or when debugging the static shell for a
// non-prerendered URL, use the prerender manifest's fallback route
// params which correctly identifies which params are unknown.
((isProduction && getRequestMeta(req, 'renderFallbackShell')) ||
hasPlaceholderFallbackRouteParams ||
(isDebugStaticShell && !isPrerendered)) &&
fallbackRouteParamsForRender
? createOpaqueFallbackRouteParams(fallbackRouteParamsForRender)
: // For intermediate shells where some params are resolved and
// others still have placeholders, use the filtered subset so the
// prerender suspends only for the unresolved params.
effectiveFallbackRouteParams &&
effectiveFallbackRouteParams.length > 0 &&
effectiveFallbackRouteParams.length <
(prerenderInfo?.fallbackRouteParams?.length ?? 0)
? createOpaqueFallbackRouteParams(effectiveFallbackRouteParams)
: isDebugFallbackShell
? getFallbackRouteParams(normalizedSrcPage, routeModule)
: null
// For staged dynamic rendering (Cached Navigations) and debug static
// shell rendering, pass the fallback params via request meta so the
// RequestStore knows which params to defer. We don't pass them as
// fallbackRouteParams because that would replace actual param values
// with opaque placeholders during segment resolution.
if (
(isProduction || isDebugStaticShell) &&
nextConfig.cacheComponents &&
!isPrerendered &&
prerenderInfo?.fallbackRouteParams
) {
const fallbackParams = createOpaqueFallbackRouteParams(
fallbackRouteParamsForRender ?? prerenderInfo.fallbackRouteParams
)
if (fallbackParams) {
addRequestMeta(req, 'fallbackParams', fallbackParams)
}
}
// Perform the render.
return doRender({
span,
postponed,
fallbackRouteParams,
forceStaticRender,
})
} catch (err) {
// if this is a background revalidate we need to report
// the request error here as it won't be bubbled
if (previousIncrementalCacheEntry?.isStale) {
const silenceLog = false
await routeModule.onRequestError(
req,
err,
{
routerKind: 'App Router',
routePath: srcPage,
routeType: 'render',
revalidateReason: getRevalidateReason({
isStaticGeneration: isSSG,
isOnDemandRevalidate,
}),
},
silenceLog,
routerServerContext
)
}
throw err
}
}
const handleResponse = async (span?: Span): Promise<null | void> => {
const cacheEntry = await routeModule.handleResponse({
cacheKey: ssgCacheKey,
responseGenerator: (c) =>
responseGenerator({
span,
...c,
}),
routeKind: RouteKind.APP_PAGE,
isOnDemandRevalidate,
isRoutePPREnabled,
req,
nextConfig,
prerenderManifest,
waitUntil: ctx.waitUntil,
isMinimalMode,
})
if (isDraftMode) {
res.setHeader(
'Cache-Control',
'private, no-cache, no-store, max-age=0, must-revalidate'
)
}
// In dev, we should not cache pages for any reason.
if (routeModule.isDev) {
res.setHeader('Cache-Control', 'no-cache, must-revalidate')
}
if (!cacheEntry) {
if (ssgCacheKey) {
// 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 null
}
if (cacheEntry.value?.kind !== CachedRouteKind.APP_PAGE) {
throw new Error(
`Invariant app-page handler received invalid cache entry ${cacheEntry.value?.kind}`
)
}
const didPostpone = typeof cacheEntry.value.postponed === 'string'
// Set the build ID header for RSC navigation requests when deploymentId is configured. This
// corresponds with maybeAppendBuildIdToRSCPayload in app-render.tsx which omits the build ID
// from the RSC payload when deploymentId is set (relying on this header instead). Server
// actions are excluded here because action redirect responses get the deployment ID header
// from the pre-fetched redirect target (via createRedirectRenderResult in action-handler.ts
// which copies headers from the internal RSC fetch).
// For static prerenders served from CDN, routes-manifest.json adds a header.
if (isRSCRequest && !isPossibleServerAction && deploymentId) {
res.setHeader(NEXT_NAV_DEPLOYMENT_ID_HEADER, deploymentId)
}
if (
isSSG &&
// We don't want to send a cache header for requests that contain dynamic
// data. If this is a Dynamic RSC request or wasn't a Prefetch RSC
// request, then we should set the cache header.
!isDynamicRSCRequest &&
(!didPostpone || isPrefetchRSCRequest)
) {
if (!isMinimalMode) {
// set x-nextjs-cache header to match the header
// we set for the image-optimizer
res.setHeader(
'x-nextjs-cache',
isOnDemandRevalidate
? 'REVALIDATED'
: cacheEntry.isMiss
? 'MISS'
: cacheEntry.isStale
? 'STALE'
: 'HIT'
)
}
// Set a header used by the client router to signal the response is static
// and should respect the `static` cache staleTime value.
res.setHeader(NEXT_IS_PRERENDER_HEADER, '1')
}
const { value: cachedData } = cacheEntry
// Coerce the cache control parameter from the render.
let cacheControl: CacheControl | undefined
// If this is a resume request in minimal mode it is streamed with dynamic
// content and should not be cached.
if (minimalPostponed) {
cacheControl = { revalidate: 0, expire: undefined }
}
// If this is in minimal mode and this is a flight request that isn't a
// prefetch request while PPR is enabled, it cannot be cached as it contains
// dynamic content.
else if (isDynamicRSCRequest) {
cacheControl = { revalidate: 0, expire: undefined }
} else if (!routeModule.isDev) {
// If this is a preview mode request, we shouldn't cache it
if (isDraftMode) {
cacheControl = { revalidate: 0, expire: undefined }
}
// If this isn't SSG, then we should set change the header only if it is
// not set already.
else if (!isSSG) {
if (!res.getHeader('Cache-Control')) {
cacheControl = { revalidate: 0, expire: undefined }
}
} else if (cacheEntry.cacheControl) {
// If the cache entry has a cache control with a revalidate value that's
// a number, use it.
if (typeof cacheEntry.cacheControl.revalidate === 'number') {
if (cacheEntry.cacheControl.revalidate < 1) {
throw new Error(
`Invalid revalidate configuration provided: ${cacheEntry.cacheControl.revalidate} < 1`
)
}
cacheControl = {
revalidate: cacheEntry.cacheControl.revalidate,
expire: cacheEntry.cacheControl.expire,
}
}
// Otherwise if the revalidate value is false, then we should use the
// cache time of one year.
else {
cacheControl = {
revalidate: CACHE_ONE_YEAR_SECONDS,
expire: undefined,
}
}
}
}
cacheEntry.cacheControl = cacheControl
if (
typeof segmentPrefetchHeader === 'string' &&
cachedData?.kind === CachedRouteKind.APP_PAGE &&
cachedData.segmentData
) {
// This is a prefetch request issued by the client Segment Cache. These
// should never reach the application layer (lambda). We should either
// respond from the cache (HIT) or respond with 404 (MISS).
// Set a header to indicate that PPR is enabled for this route. This
// lets the client distinguish between a regular cache miss and a cache
// miss due to PPR being disabled. In other contexts this header is used
// to indicate that the response contains dynamic data, but here we're
// only using it to indicate that the feature is enabled — the segment
// response itself contains whether the data is dynamic.
res.setHeader(NEXT_DID_POSTPONE_HEADER, '2')
// Add the cache tags header to the response if it exists and we're in
// minimal mode while rendering a static page.
const tags = cachedData.headers?.[NEXT_CACHE_TAGS_HEADER]
if (isMinimalMode && isSSG && tags && typeof tags === 'string') {
res.setHeader(NEXT_CACHE_TAGS_HEADER, tags)
}
const matchedSegment = cachedData.segmentData.get(segmentPrefetchHeader)
if (matchedSegment !== undefined) {
// Cache hit
return sendRenderResult({
req,
res,
generateEtags: nextConfig.generateEtags,
poweredByHeader: nextConfig.poweredByHeader,
result: RenderResult.fromStatic(
matchedSegment,
RSC_CONTENT_TYPE_HEADER
),
cacheControl: cacheEntry.cacheControl,
})
}
// Cache miss. Either a cache entry for this route has not been generated
// (which technically should not be possible when PPR is enabled, because
// at a minimum there should always be a fallback entry) or there's no
// match for the requested segment. Respond with a 404.
res.statusCode = 404
return sendRenderResult({
req,
res,
generateEtags: nextConfig.generateEtags,
poweredByHeader: nextConfig.poweredByHeader,
result: RenderResult.EMPTY,
cacheControl: cacheEntry.cacheControl,
})
}
// If there's a callback for `onCacheEntry`, call it with the cache entry
// and the revalidate options. If we support RDC for Navigations, we
// prefer the `onCacheEntryV2` callback. Once RDC for Navigations is the
// default, we can remove the fallback to `onCacheEntry` as
// `onCacheEntryV2` is now fully supported.
const onCacheEntry = supportsRDCForNavigations
? (getRequestMeta(req, 'onCacheEntryV2') ??
getRequestMeta(req, 'onCacheEntry'))
: getRequestMeta(req, 'onCacheEntry')
if (onCacheEntry) {
const rawCacheEntryUrl = getRequestMeta(req, 'initURL') ?? req.url
const cacheEntryUrl = rawCacheEntryUrl
? (parseUrl(rawCacheEntryUrl)?.pathname ?? rawCacheEntryUrl)
: undefined
const finished = await onCacheEntry(cacheEntry, {
url: cacheEntryUrl,
})
if (finished) return null
}
if (cachedData.headers) {
const headers = { ...cachedData.headers }
if (!isMinimalMode || !isSSG) {
delete headers[NEXT_CACHE_TAGS_HEADER]
}
for (let [key, value] of Object.entries(headers)) {
if (typeof value === 'undefined') continue
if (Array.isArray(value)) {
for (const v of value) {
res.appendHeader(key, v)
}
} else if (typeof value === 'number') {
value = value.toString()
res.appendHeader(key, value)
} else {
res.appendHeader(key, value)
}
}
}
// Add the cache tags header to the response if it exists and we're in
// minimal mode while rendering a static page.
const tags = cachedData.headers?.[NEXT_CACHE_TAGS_HEADER]
if (isMinimalMode && isSSG && tags && typeof tags === 'string') {
res.setHeader(NEXT_CACHE_TAGS_HEADER, tags)
}
// If the request is a data request, then we shouldn't set the status code
// from the response because it should always be 200. This should be gated
// behind the experimental PPR flag.
if (cachedData.status && (!isRSCRequest || !isRoutePPREnabled)) {
res.statusCode = cachedData.status
}
// Redirect information is encoded in RSC payload, so we don't need to use redirect status codes
if (
!isMinimalMode &&
cachedData.status &&
RedirectStatusCode[cachedData.status] &&
isRSCRequest
) {
res.statusCode = 200
}
// Mark that the request did postpone.
if (didPostpone && !isDynamicRSCRequest) {
res.setHeader(NEXT_DID_POSTPONE_HEADER, '1')
}
// we don't go through this block when preview mode is true
// as preview mode is a dynamic request (bypasses cache) and doesn't
// generate both HTML and payloads in the same request so continue to just
// return the generated payload
if (isRSCRequest && !isDraftMode) {
// If this is a dynamic RSC request, then stream the response.
if (typeof cachedData.rscData === 'undefined') {
// If the response is not an RSC response, then we can't serve it.
if (cachedData.html.contentType !== RSC_CONTENT_TYPE_HEADER) {
if (nextConfig.cacheComponents) {
res.statusCode = 404
return sendRenderResult({
req,
res,
generateEtags: nextConfig.generateEtags,
poweredByHeader: nextConfig.poweredByHeader,
result: RenderResult.EMPTY,
cacheControl: cacheEntry.cacheControl,
})
} else {
// Otherwise this case is not expected.
throw new InvariantError(
`Expected RSC response, got ${cachedData.html.contentType}`
)
}
}
return sendRenderResult({
req,
res,
generateEtags: nextConfig.generateEtags,
poweredByHeader: nextConfig.poweredByHeader,
result: cachedData.html,
cacheControl: cacheEntry.cacheControl,
})
}
// As this isn't a prefetch request, we should serve the static flight
// data.
return sendRenderResult({
req,
res,
generateEtags: nextConfig.generateEtags,
poweredByHeader: nextConfig.poweredByHeader,
result: RenderResult.fromStatic(
cachedData.rscData,
RSC_CONTENT_TYPE_HEADER
),
cacheControl: cacheEntry.cacheControl,
})
}
// This is a request for HTML data.
const body = cachedData.html
// Instant Navigation Testing API: serve the static shell with an
// injected script that sets self.__next_instant_test and kicks off a
// static RSC fetch for hydration. The transform stream also appends
// closing </body></html> tags so the browser can parse the full document.
// In dev mode, also inject self.__next_r so the HMR WebSocket and
// debug channel can initialize.
if (isInstantNavigationTest && isDebugStaticShell) {
const instantTestRequestId =
routeModule.isDev === true ? crypto.randomUUID() : null
body.pipeThrough(
await createInstantTestScriptInsertionTransformStream(
instantTestRequestId
)
)
return sendRenderResult({
req,
res,
generateEtags: nextConfig.generateEtags,
poweredByHeader: nextConfig.poweredByHeader,
result: body,
cacheControl: { revalidate: 0, expire: undefined },
})
}
// If there's no postponed state, we should just serve the HTML. This
// should also be the case for a resume request because it's completed
// as a server render (rather than a static render).
if (!didPostpone || isMinimalMode || isRSCRequest) {
// If we're in test mode, we should add a sentinel chunk to the response
// that's between the static and dynamic parts so we can compare the
// chunks and add assertions.
if (
process.env.__NEXT_TEST_MODE &&
isMinimalMode &&
isRoutePPREnabled &&
body.contentType === HTML_CONTENT_TYPE_HEADER
) {
// As we're in minimal mode, the static part would have already been
// streamed first. The only part that this streams is the dynamic part
// so we should FIRST stream the sentinel and THEN the dynamic part.
body.unshift(createPPRBoundarySentinel())
}
return sendRenderResult({
req,
res,
generateEtags: nextConfig.generateEtags,
poweredByHeader: nextConfig.poweredByHeader,
result: body,
cacheControl: cacheEntry.cacheControl,
})
}
// If we're debugging the static shell or the dynamic API accesses, we
// should just serve the HTML without resuming the render. The returned
// HTML will be the static shell so all the Dynamic API's will be used
// during static generation.
if (isDebugStaticShell || isDebugDynamicAccesses) {
// Since we're not resuming the render, we need to at least add the
// closing body and html tags to create valid HTML.
body.push(
new ReadableStream({
start(controller) {
controller.enqueue(ENCODED_TAGS.CLOSED.BODY_AND_HTML)
controller.close()
},
})
)
return sendRenderResult({
req,
res,
generateEtags: nextConfig.generateEtags,
poweredByHeader: nextConfig.poweredByHeader,
result: body,
cacheControl: { revalidate: 0, expire: undefined },
})
}
// If we're in test mode, we should add a sentinel chunk to the response
// that's between the static and dynamic parts so we can compare the
// chunks and add assertions.
if (process.env.__NEXT_TEST_MODE) {
body.push(createPPRBoundarySentinel())
}
// This request has postponed, so let's create a new transformer that the
// dynamic data can pipe to that will attach the dynamic data to the end
// of the response.
const transformer = new TransformStream<Uint8Array, Uint8Array>()
body.push(transformer.readable)
// Perform the render again, but this time, provide the postponed state.
// We don't await because we want the result to start streaming now, and
// we've already chained the transformer's readable to the render result.
doRender({
span,
postponed: cachedData.postponed,
// This is a resume render, not a fallback render, so we don't need to
// set this.
fallbackRouteParams: null,
forceStaticRender: false,
})
.then(async (result) => {
if (!result) {
throw new Error('Invariant: expected a result to be returned')
}
if (result.value?.kind !== CachedRouteKind.APP_PAGE) {
throw new Error(
`Invariant: expected a page response, got ${result.value?.kind}`
)
}
// Pipe the resume result to the transformer.
await result.value.html.pipeTo(transformer.writable)
})
.catch((err) => {
// An error occurred during piping or preparing the render, abort
// the transformers writer so we can terminate the stream.
transformer.writable.abort(err).catch((e) => {
console.error("couldn't abort transformer", e)
})
})
return sendRenderResult({
req,
res,
generateEtags: nextConfig.generateEtags,
poweredByHeader: nextConfig.poweredByHeader,
result: body,
// We don't want to cache the response if it has postponed data because
// the response being sent to the client it's dynamic parts are streamed
// to the client on the same request.
cacheControl: { revalidate: 0, expire: undefined },
})
}
// TODO: activeSpan code path is for when wrapped by
// next-server can be removed when this is no longer used
if (isWrappedByNextServer && activeSpan) {
await handleResponse(activeSpan)
} else {
parentSpan = tracer.getActiveScopeSpan()
return await tracer.withPropagatedContext(
req.headers,
() =>
tracer.trace(
BaseServerSpan.handleRequest,
{
spanName: `${method} ${srcPage}`,
kind: SpanKind.SERVER,
attributes: {
'http.method': method,
'http.target': req.url,
},
},
handleResponse
),
undefined,
!isWrappedByNextServer
)
}
} catch (err) {
if (!(err instanceof NoFallbackError)) {
const silenceLog = false
await routeModule.onRequestError(
req,
err,
{
routerKind: 'App Router',
routePath: srcPage,
routeType: 'render',
revalidateReason: getRevalidateReason({
isStaticGeneration: isSSG,
isOnDemandRevalidate,
}),
},
silenceLog,
routerServerContext
)
}
// rethrow so that we can handle serving error page
throw err
}
}
// TODO: omit this from production builds, only test builds should include it
/**
* Creates a readable stream that emits a PPR boundary sentinel.
*
* @returns A readable stream that emits a PPR boundary sentinel.
*/
function createPPRBoundarySentinel() {
return new ReadableStream({
start(controller) {
controller.enqueue(
new TextEncoder().encode('<!-- PPR_BOUNDARY_SENTINEL -->')
)
controller.close()
},
})
}