import {
AppRouteRouteModule,
type AppRouteRouteHandlerContext,
type AppRouteRouteModuleOptions,
} from '../../server/route-modules/app-route/module.compiled'
import { RouteKind } from '../../server/route-kind'
import { patchFetch as _patchFetch } from '../../server/lib/patch-fetch'
import type { IncomingMessage, ServerResponse } from 'node:http'
import {
addRequestMeta,
getRequestMeta,
setRequestMeta,
type RequestMeta,
} from '../../server/request-meta'
import { getTracer, type Span, SpanKind } from '../../server/lib/trace/tracer'
import { setManifestsSingleton } from '../../server/app-render/manifests-singleton'
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
import { NodeNextRequest, NodeNextResponse } from '../../server/base-http/node'
import {
NextRequestAdapter,
signalFromNodeResponse,
} from '../../server/web/spec-extension/adapters/next-request'
import { BaseServerSpan } from '../../server/lib/trace/constants'
import { getRevalidateReason } from '../../server/instrumentation/utils'
import { sendResponse } from '../../server/send-response'
import {
fromNodeOutgoingHttpHeaders,
toNodeOutgoingHttpHeaders,
} from '../../server/web/utils'
import { getCacheControlHeader } from '../../server/lib/cache-control'
import { INFINITE_CACHE, NEXT_CACHE_TAGS_HEADER } from '../../lib/constants'
import { NoFallbackError } from '../../shared/lib/no-fallback-error.external'
import {
CachedRouteKind,
type ResponseCacheEntry,
type ResponseGenerator,
} from '../../server/response-cache'
// These are injected by the loader afterwards. This is injected as a variable
// instead of a replacement because this could also be `undefined` instead of
// an empty string.
declare const nextConfigOutput: AppRouteRouteModuleOptions['nextConfigOutput']
// We inject the nextConfigOutput here so that we can use them in the route
// module.
// INJECT:nextConfigOutput
const routeModule = new AppRouteRouteModule({
definition: {
kind: RouteKind.APP_ROUTE,
page: 'VAR_DEFINITION_PAGE',
pathname: 'VAR_DEFINITION_PATHNAME',
filename: 'VAR_DEFINITION_FILENAME',
bundlePath: 'VAR_DEFINITION_BUNDLE_PATH',
},
distDir: process.env.__NEXT_RELATIVE_DIST_DIR || '',
relativeProjectDir: process.env.__NEXT_RELATIVE_PROJECT_DIR || '',
resolvedPagePath: 'VAR_RESOLVED_PAGE_PATH',
nextConfigOutput,
// Always use a lazy require factory so that:
// - In dev: devRequestTimingInternalsEnd is set before userland executes,
// correctly attributing module load time to application-code rather than
// framework internals.
// - In all modes: async modules (route files with top-level await) are
// handled correctly — require() returns a Promise for such modules, which
// ensureUserland() awaits before the first request is handled. Eagerly
// calling require() would pass that Promise directly to the constructor
// and break _initFromUserland().
userland: () => require('VAR_USERLAND') as typeof import('VAR_USERLAND'),
// In Turbopack dev mode, also provide a synchronous per-request getter so
// server HMR updates are picked up without re-executing the entry chunk.
// Using require() (synchronous) avoids adding async overhead that would be
// incorrectly attributed to application-code time in devRequestTiming.
...(process.env.TURBOPACK && process.env.__NEXT_DEV_SERVER
? {
getUserland: () =>
require('VAR_USERLAND') as typeof import('VAR_USERLAND'),
}
: {}),
})
// Pull out the exports that we need to expose from the module. This should
// be eliminated when we've moved the other routes to the new format. These
// are used to hook into the route.
const { workAsyncStorage, workUnitAsyncStorage, serverHooks } = routeModule
function patchFetch() {
return _patchFetch({
workAsyncStorage,
workUnitAsyncStorage,
})
}
export {
routeModule,
workAsyncStorage,
workUnitAsyncStorage,
serverHooks,
patchFetch,
}
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())
}
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,
deploymentId,
params,
nextConfig,
parsedUrl,
isDraftMode,
prerenderManifest,
routerServerContext,
isOnDemandRevalidate,
revalidateOnlyGenerated,
resolvedPathname,
clientReferenceManifest,
serverActionsManifest,
} = prepareResult
const normalizedSrcPage = normalizeAppPath(srcPage)
let isIsr = Boolean(
prerenderManifest.dynamicRoutes[normalizedSrcPage] ||
prerenderManifest.routes[resolvedPathname]
)
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
}
if (isIsr && !isDraftMode) {
const isPrerendered = Boolean(prerenderManifest.routes[resolvedPathname])
const prerenderInfo = prerenderManifest.dynamicRoutes[normalizedSrcPage]
if (prerenderInfo) {
if (prerenderInfo.fallback === false && !isPrerendered) {
if (nextConfig.adapterPath) {
return await render404()
}
throw new NoFallbackError()
}
}
}
let cacheKey: string | null = null
if (isIsr && !routeModule.isDev && !isDraftMode) {
cacheKey = resolvedPathname
// ensure /index and / is normalized to one key
cacheKey = cacheKey === '/index' ? '/' : cacheKey
}
const supportsDynamicResponse: boolean =
// If we're in development, we always support dynamic HTML
routeModule.isDev === true ||
// If this is not SSG or does not have static paths, then it supports
// dynamic HTML.
!isIsr
// This is a revalidation request if the request is for a static
// page and it is not being resumed from a postponed render and
// it is not a dynamic RSC request then it is a revalidation
// request.
const isStaticGeneration = isIsr && !supportsDynamicResponse
// 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 isMinimalMode = Boolean(getRequestMeta(req, 'minimalMode'))
const incrementalCache =
getRequestMeta(req, 'incrementalCache') ||
(await routeModule.getIncrementalCache(
req,
nextConfig,
prerenderManifest,
isMinimalMode
))
incrementalCache?.resetRequestCache()
;(globalThis as any).__incrementalCache = incrementalCache
const context: AppRouteRouteHandlerContext = {
params,
previewProps: prerenderManifest.preview,
renderOpts: {
experimental: {
authInterrupts: Boolean(nextConfig.experimental.authInterrupts),
useCacheTimeout: nextConfig.experimental.useCacheTimeout,
},
cacheComponents: Boolean(nextConfig.cacheComponents),
supportsDynamicResponse,
incrementalCache,
cacheLifeProfiles: nextConfig.cacheLife,
staticPageGenerationTimeout: nextConfig.staticPageGenerationTimeout,
waitUntil: ctx.waitUntil,
onClose: (cb) => {
res.on('close', cb)
},
onAfterTaskError: undefined,
onInstrumentationRequestError: (
error,
_request,
errorContext,
silenceLog
) =>
routeModule.onRequestError(
req,
error,
errorContext,
silenceLog,
routerServerContext
),
},
sharedContext: {
buildId,
deploymentId,
},
}
const nodeNextReq = new NodeNextRequest(req)
const nodeNextRes = new NodeNextResponse(res)
const nextReq = NextRequestAdapter.fromNodeNextRequest(
nodeNextReq,
signalFromNodeResponse(res)
)
try {
let parentSpan: Span | undefined
const invokeRouteModule = async (span?: Span) => {
return routeModule.handle(nextReq, 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 handleResponse = async (currentSpan?: Span) => {
const responseGenerator: ResponseGenerator = async ({
previousCacheEntry,
}) => {
try {
if (
!isMinimalMode &&
isOnDemandRevalidate &&
revalidateOnlyGenerated &&
!previousCacheEntry
) {
res.statusCode = 404
// on-demand revalidate always sets this header
res.setHeader('x-nextjs-cache', 'REVALIDATED')
res.end('This page could not be found')
return null
}
const response = await invokeRouteModule(currentSpan)
;(req as any).fetchMetrics = (context.renderOpts as any).fetchMetrics
let pendingWaitUntil = context.renderOpts.pendingWaitUntil
// Attempt using provided waitUntil if available
// if it's not we fallback to sendResponse's handling
if (pendingWaitUntil) {
if (ctx.waitUntil) {
ctx.waitUntil(pendingWaitUntil)
pendingWaitUntil = undefined
}
}
const cacheTags = context.renderOpts.collectedTags
// If the request is for a static response, we can cache it so long
// as it's not edge.
if (isIsr) {
const blob = await response.blob()
// Copy the headers from the response.
const headers = toNodeOutgoingHttpHeaders(response.headers)
if (cacheTags) {
headers[NEXT_CACHE_TAGS_HEADER] = cacheTags
}
if (!headers['content-type'] && blob.type) {
headers['content-type'] = blob.type
}
const revalidate =
typeof context.renderOpts.collectedRevalidate === 'undefined' ||
context.renderOpts.collectedRevalidate >= INFINITE_CACHE
? false
: context.renderOpts.collectedRevalidate
const expire =
typeof context.renderOpts.collectedExpire === 'undefined' ||
context.renderOpts.collectedExpire >= INFINITE_CACHE
? // Fall back to the global `expireTime` config when the
// route has a numeric `revalidate` but didn't declare an
// explicit `expire` (e.g. via `cacheLife`). This mirrors the
// build-time fallback in `build/index.ts` so cache entries
// and the response Cache-Control header agree on the route's
// effective expire. Routes that opt out of revalidation
// (`revalidate: false`) or that are dynamic (`revalidate: 0`)
// keep `expire: undefined`.
revalidate !== false && revalidate > 0
? nextConfig.expireTime
: undefined
: context.renderOpts.collectedExpire
// Create the cache entry for the response.
const cacheEntry: ResponseCacheEntry = {
value: {
kind: CachedRouteKind.APP_ROUTE,
status: response.status,
body: Buffer.from(await blob.arrayBuffer()),
headers,
},
cacheControl: { revalidate, expire },
}
return cacheEntry
} else {
// send response without caching if not ISR
await sendResponse(
nodeNextReq,
nodeNextRes,
response,
pendingWaitUntil
)
return null
}
} catch (err) {
// if this is a background revalidate we need to report
// the request error here as it won't be bubbled
if (previousCacheEntry?.isStale) {
const silenceLog = false
await routeModule.onRequestError(
req,
err,
{
routerKind: 'App Router',
routePath: srcPage,
routeType: 'route',
revalidateReason: getRevalidateReason({
isStaticGeneration,
isOnDemandRevalidate,
}),
},
silenceLog,
routerServerContext
)
}
throw err
}
}
const cacheEntry = await routeModule.handleResponse({
req,
nextConfig,
cacheKey,
routeKind: RouteKind.APP_ROUTE,
isFallback: false,
prerenderManifest,
isRoutePPREnabled: false,
isOnDemandRevalidate,
revalidateOnlyGenerated,
responseGenerator,
waitUntil: ctx.waitUntil,
isMinimalMode,
})
// we don't create a cacheEntry for ISR
if (!isIsr) {
return null
}
if (cacheEntry?.value?.kind !== CachedRouteKind.APP_ROUTE) {
throw new Error(
`Invariant: app-route received invalid cache entry ${cacheEntry?.value?.kind}`
)
}
if (!isMinimalMode) {
res.setHeader(
'x-nextjs-cache',
isOnDemandRevalidate
? 'REVALIDATED'
: cacheEntry.isMiss
? 'MISS'
: cacheEntry.isStale
? 'STALE'
: 'HIT'
)
}
// Draft mode should never be cached
if (isDraftMode) {
res.setHeader(
'Cache-Control',
'private, no-cache, no-store, max-age=0, must-revalidate'
)
}
const headers = fromNodeOutgoingHttpHeaders(cacheEntry.value.headers)
if (!(isMinimalMode && isIsr)) {
headers.delete(NEXT_CACHE_TAGS_HEADER)
}
// If cache control is already set on the response we don't
// override it to allow users to customize it via next.config
if (
cacheEntry.cacheControl &&
!res.getHeader('Cache-Control') &&
!headers.get('Cache-Control')
) {
headers.set(
'Cache-Control',
getCacheControlHeader(cacheEntry.cacheControl)
)
}
await sendResponse(
nodeNextReq,
nodeNextRes,
// @ts-expect-error - Argument of type 'Buffer<ArrayBufferLike>' is not assignable to parameter of type 'BodyInit | null | undefined'.
new Response(cacheEntry.value.body, {
headers,
status: cacheEntry.value.status || 200,
})
)
return null
}
// 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()
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: normalizedSrcPage,
routeType: 'route',
revalidateReason: getRevalidateReason({
isStaticGeneration,
isOnDemandRevalidate,
}),
},
silenceLog,
routerServerContext
)
}
// rethrow so that we can handle serving error page
// If this is during static generation, throw the error again.
if (isIsr) throw err
// Otherwise, send a 500 response.
await sendResponse(
nodeNextReq,
nodeNextRes,
new Response(null, { status: 500 })
)
return null
}
}