1241 lines
74 KiB
JavaScript
1241 lines
74 KiB
JavaScript
import { AppPageRouteModule } 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';
|
|
import { getTracer, SpanKind } from '../../server/lib/trace/tracer';
|
|
import { addRequestMeta, getRequestMeta, setRequestMeta } from '../../server/request-meta';
|
|
import { BaseServerSpan } from '../../server/lib/trace/constants';
|
|
import { interopDefault } from '../../server/app-render/interop-default';
|
|
import { stripFlightHeaders } from '../../server/app-render/strip-flight-headers';
|
|
import { NodeNextRequest, NodeNextResponse } from '../../server/base-http/node';
|
|
import { checkIsAppPPREnabled } from '../../server/lib/experimental/ppr';
|
|
import { getFallbackRouteParams, createOpaqueFallbackRouteParams } from '../../server/request/fallback-params';
|
|
import { setManifestsSingleton } from '../../server/app-render/manifests-singleton';
|
|
import { isHtmlBotRequest, shouldServeStreamingMetadata } from '../../server/lib/streaming-metadata';
|
|
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths';
|
|
import { getIsPossibleServerAction } from '../../server/lib/server-action-request-meta';
|
|
import { RSC_HEADER, NEXT_ROUTER_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';
|
|
import { getBotType, isBot } from '../../shared/lib/router/utils/is-bot';
|
|
import { CachedRouteKind, IncrementalCacheKind } from '../../server/response-cache';
|
|
import { FallbackMode, parseFallbackField } from '../../lib/fallback';
|
|
import RenderResult from '../../server/render-result';
|
|
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';
|
|
import { ENCODED_TAGS } from '../../server/stream-utils/encoded-tags';
|
|
import { createInstantTestScriptInsertionTransformStream } from '../../server/stream-utils/node-web-streams-helper';
|
|
import { sendRenderResult } from '../../server/send-payload';
|
|
import { NoFallbackError } from '../../shared/lib/no-fallback-error.external';
|
|
import { parseMaxPostponedStateSize } from '../../shared/lib/size-limit';
|
|
import { getMaxPostponedStateSize, getPostponedStateExceededErrorMessage, readBodyWithSizeLimit } from '../../server/lib/postponed-request-body';
|
|
// 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';
|
|
import { InvariantError } from '../../shared/lib/invariant-error';
|
|
import { scheduleOnNextTick } from '../../lib/scheduler';
|
|
import { isInterceptionRouteAppPath } from '../../shared/lib/router/utils/interception-routes';
|
|
import { getParamProperties, getSegmentParam } from '../../shared/lib/router/utils/get-segment-param';
|
|
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 || ''
|
|
});
|
|
function buildDynamicSegmentPlaceholder(param) {
|
|
const { repeat, optional } = getParamProperties(param.paramType);
|
|
if (optional) {
|
|
return `[[...${param.paramName}]]`;
|
|
}
|
|
if (repeat) {
|
|
return `[...${param.paramName}]`;
|
|
}
|
|
return `[${param.paramName}]`;
|
|
}
|
|
/**
|
|
* 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, remainingPrerenderableParams, params) {
|
|
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 == null ? void 0 : 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, res, ctx) {
|
|
var _this, _prerenderManifest_routes_resolvedPathname, _prerenderInfo_fallbackRootParams, _prerenderInfo_fallbackRouteParams;
|
|
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;
|
|
const prepareResult = await routeModule.prepare(req, res, {
|
|
srcPage,
|
|
multiZoneDraftMode
|
|
});
|
|
if (!prepareResult) {
|
|
res.statusCode = 400;
|
|
res.end('Bad Request');
|
|
ctx.waitUntil == null ? void 0 : ctx.waitUntil.call(ctx, Promise.resolve());
|
|
return null;
|
|
}
|
|
const { buildId, query, params, pageIsDynamic, buildManifest, nextFontManifest, reactLoadableManifest, serverActionsManifest, clientReferenceManifest, subresourceIntegrityManifest, prerenderManifest, 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 == null ? void 0 : 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 = 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) {
|
|
var _nextConfig_experimental_serverActions;
|
|
if (stateLength > maxPostponedStateSizeBytes) {
|
|
res.statusCode = 413;
|
|
res.end(getPostponedStateExceededErrorMessage(maxPostponedStateSize));
|
|
ctx.waitUntil == null ? void 0 : ctx.waitUntil.call(ctx, 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 = nextConfig.experimental.serverActions) == null ? void 0 : _nextConfig_experimental_serverActions.bodySizeLimit) ?? defaultActionBodySizeLimit;
|
|
const actionBodySizeLimitBytes = actionBodySizeLimit !== defaultActionBodySizeLimit ? require('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 == null ? void 0 : ctx.waitUntil.call(ctx, 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 Object.defineProperty(new Error(`invariant: expected ${stateLength} bytes of postponed state but only received ${fullBody.length} bytes`), "__NEXT_ERROR_CODE", {
|
|
value: "E979",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
}
|
|
}
|
|
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 == null ? void 0 : ctx.waitUntil.call(ctx, 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 = // 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) && (((_this = prerenderManifest.routes[normalizedSrcPage] ?? prerenderManifest.dynamicRoutes[normalizedSrcPage]) == null ? void 0 : _this.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 == null ? void 0 : routerServerContext.experimentalTestProxy) === true));
|
|
const isDebugStaticShell = (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 = prerenderManifest.routes[resolvedPathname]) == null ? void 0 : _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 segmentPrefetchHeader = getRequestMeta(req, 'segmentPrefetchRSCRequest');
|
|
// 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 = // 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 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') ? // 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 : 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 == null ? void 0 : prerenderInfo.remainingPrerenderableParams) ?? [];
|
|
const hasUnresolvedRootFallbackParams = (prerenderInfo == null ? void 0 : prerenderInfo.fallback) === null && (((_prerenderInfo_fallbackRootParams = prerenderInfo.fallbackRootParams) == null ? void 0 : _prerenderInfo_fallbackRootParams.length) ?? 0) > 0;
|
|
let ssgCacheKey = 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 == null ? void 0 : prerenderInfo.fallback) === 'string' ? prerenderInfo.fallback : prerenderMatch.source : null;
|
|
if (nextConfig.experimental.partialFallbacks === true && fallbackPathname && (prerenderInfo == null ? void 0 : prerenderInfo.fallbackRouteParams) && !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 == null ? void 0 : prerenderInfo.fallbackRouteParams))) {
|
|
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 == null ? void 0 : routerServerContext.isWrappedByNextServer);
|
|
const remainingFallbackRouteParams = nextConfig.experimental.partialFallbacks === true && remainingPrerenderableParams.length > 0 ? (prerenderInfo == null ? void 0 : (_prerenderInfo_fallbackRouteParams = prerenderInfo.fallbackRouteParams) == null ? void 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 == null ? void 0 : 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;
|
|
const invokeRouteModule = async (span, context)=>{
|
|
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');
|
|
if (route) {
|
|
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);
|
|
}
|
|
} else {
|
|
span.updateName(`${method} ${srcPage}`);
|
|
}
|
|
});
|
|
};
|
|
const incrementalCache = getRequestMeta(req, 'incrementalCache') || await routeModule.getIncrementalCache(req, nextConfig, prerenderManifest, isMinimalMode);
|
|
incrementalCache == null ? void 0 : incrementalCache.resetRequestCache();
|
|
globalThis.__incrementalCache = incrementalCache;
|
|
const doRender = async ({ span, postponed, fallbackRouteParams, forceStaticRender })=>{
|
|
const context = {
|
|
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 == null ? void 0 : routerServerContext.setCacheStatus,
|
|
setIsrStatus: routerServerContext == null ? void 0 : routerServerContext.setIsrStatus,
|
|
setReactDebugChannel: routerServerContext == null ? void 0 : routerServerContext.setReactDebugChannel,
|
|
sendErrorsToBrowser: routerServerContext == null ? void 0 : routerServerContext.sendErrorsToBrowser,
|
|
dir: process.env.NEXT_RUNTIME === 'nodejs' ? require('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,
|
|
incrementalCache,
|
|
cacheLifeProfiles: nextConfig.cacheLife,
|
|
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),
|
|
cachedNavigations: Boolean(nextConfig.experimental.cachedNavigations),
|
|
clientTraceMetadata: nextConfig.experimental.clientTraceMetadata || [],
|
|
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;
|
|
if (cacheTags) {
|
|
headers[NEXT_CACHE_TAGS_HEADER] = cacheTags;
|
|
}
|
|
// Pull any fetch metrics from the render onto the request.
|
|
;
|
|
req.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 == null ? void 0 : cacheControl.revalidate) === 0 && !routeModule.isDev && !isRoutePPREnabled) {
|
|
const staticBailoutInfo = metadata.staticBailoutInfo;
|
|
const err = Object.defineProperty(new Error(`Page changed from static to dynamic at runtime ${resolvedPathname}${(staticBailoutInfo == null ? void 0 : staticBailoutInfo.description) ? `, reason: ${staticBailoutInfo.description}` : ``}` + `\nsee more here https://nextjs.org/docs/messages/app-static-to-dynamic-error`), "__NEXT_ERROR_CODE", {
|
|
value: "E132",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
if (staticBailoutInfo == null ? void 0 : 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
|
|
},
|
|
cacheControl
|
|
};
|
|
};
|
|
const responseGenerator = async ({ hasResolved, previousCacheEntry: previousIncrementalCacheEntry, isRevalidating, span, forceStaticRender = false })=>{
|
|
const isProduction = routeModule.isDev === false;
|
|
const didRespond = hasResolved || res.writableEnded;
|
|
// skip on-demand revalidate if cache is not present and
|
|
// revalidate-if-generated is set
|
|
if (isOnDemandRevalidate && revalidateOnlyGenerated && !previousIncrementalCacheEntry && !isMinimalMode) {
|
|
if (routerServerContext == null ? void 0 : routerServerContext.render404) {
|
|
await routerServerContext.render404(req, res);
|
|
} else {
|
|
res.statusCode = 404;
|
|
res.end('This page could not be found');
|
|
}
|
|
return null;
|
|
}
|
|
let fallbackMode;
|
|
if (prerenderInfo) {
|
|
fallbackMode = parseFallbackField(prerenderInfo.fallback);
|
|
}
|
|
if (nextConfig.experimental.partialFallbacks === true && (prerenderInfo == null ? void 0 : prerenderInfo.fallback) === null && !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 == null ? void 0 : 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 == null ? void 0 : prerenderInfo.fallback) === 'string' ? prerenderInfo.fallback : normalizedSrcPage;
|
|
const fallbackRouteParams = // In production or when debugging the static shell (e.g. instant
|
|
// navigation testing), use the prerender manifest's fallback
|
|
// route params which correctly identifies which params are
|
|
// unknown. Note: in dev, this block is only entered for
|
|
// non-prerendered URLs (guarded by the outer condition).
|
|
(isProduction || isDebugStaticShell) && (prerenderInfo == null ? void 0 : prerenderInfo.fallbackRouteParams) ? createOpaqueFallbackRouteParams(prerenderInfo.fallbackRouteParams) : // fallback (simulating the worst-case shell).
|
|
isDebugFallbackShell ? getFallbackRouteParams(normalizedSrcPage, routeModule) : 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 && // Avoid background revalidate during prefetches; this can trigger
|
|
// static prerender errors that surface as 500s for the prefetch
|
|
// request itself.
|
|
!isPrefetchRSCRequest) {
|
|
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 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.
|
|
if (// 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
|
|
}
|
|
};
|
|
}
|
|
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') || isDebugStaticShell && !isPrerendered) && (prerenderInfo == null ? void 0 : prerenderInfo.fallbackRouteParams) ? createOpaqueFallbackRouteParams(prerenderInfo.fallbackRouteParams) : 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 == null ? void 0 : prerenderInfo.fallbackRouteParams)) {
|
|
const fallbackParams = createOpaqueFallbackRouteParams(prerenderInfo.fallbackRouteParams);
|
|
if (fallbackParams) {
|
|
addRequestMeta(req, 'fallbackParams', fallbackParams);
|
|
}
|
|
}
|
|
// Perform the render.
|
|
return doRender({
|
|
span,
|
|
postponed,
|
|
fallbackRouteParams,
|
|
forceStaticRender
|
|
});
|
|
};
|
|
const handleResponse = async (span)=>{
|
|
var _cacheEntry_value, _cachedData_headers;
|
|
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 Object.defineProperty(new Error('invariant: cache entry required but not generated'), "__NEXT_ERROR_CODE", {
|
|
value: "E62",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
return null;
|
|
}
|
|
if (((_cacheEntry_value = cacheEntry.value) == null ? void 0 : _cacheEntry_value.kind) !== CachedRouteKind.APP_PAGE) {
|
|
var _cacheEntry_value1;
|
|
throw Object.defineProperty(new Error(`Invariant app-page handler received invalid cache entry ${(_cacheEntry_value1 = cacheEntry.value) == null ? void 0 : _cacheEntry_value1.kind}`), "__NEXT_ERROR_CODE", {
|
|
value: "E707",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
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;
|
|
// 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
|
|
};
|
|
} 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
|
|
};
|
|
} 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') {
|
|
var _cacheEntry_cacheControl;
|
|
if (cacheEntry.cacheControl.revalidate < 1) {
|
|
throw Object.defineProperty(new Error(`Invalid revalidate configuration provided: ${cacheEntry.cacheControl.revalidate} < 1`), "__NEXT_ERROR_CODE", {
|
|
value: "E22",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
cacheControl = {
|
|
revalidate: cacheEntry.cacheControl.revalidate,
|
|
expire: ((_cacheEntry_cacheControl = cacheEntry.cacheControl) == null ? void 0 : _cacheEntry_cacheControl.expire) ?? nextConfig.expireTime
|
|
};
|
|
} else {
|
|
cacheControl = {
|
|
revalidate: CACHE_ONE_YEAR_SECONDS,
|
|
expire: undefined
|
|
};
|
|
}
|
|
}
|
|
}
|
|
cacheEntry.cacheControl = cacheControl;
|
|
if (typeof segmentPrefetchHeader === 'string' && (cachedData == null ? void 0 : cachedData.kind) === CachedRouteKind.APP_PAGE && cachedData.segmentData) {
|
|
var _cachedData_headers1;
|
|
// 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 204 No Content (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_headers1 = cachedData.headers) == null ? void 0 : _cachedData_headers1[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 204 No Content. We
|
|
// don't bother to respond with 404, because these requests are only
|
|
// issued as part of a prefetch.
|
|
res.statusCode = 204;
|
|
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 finished = await onCacheEntry(cacheEntry, {
|
|
url: getRequestMeta(req, 'initURL') ?? req.url
|
|
});
|
|
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 = cachedData.headers) == null ? void 0 : _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 Object.defineProperty(new InvariantError(`Expected RSC response, got ${cachedData.html.contentType}`), "__NEXT_ERROR_CODE", {
|
|
value: "E789",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
}
|
|
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(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();
|
|
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)=>{
|
|
var _result_value;
|
|
if (!result) {
|
|
throw Object.defineProperty(new Error('Invariant: expected a result to be returned'), "__NEXT_ERROR_CODE", {
|
|
value: "E463",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
if (((_result_value = result.value) == null ? void 0 : _result_value.kind) !== CachedRouteKind.APP_PAGE) {
|
|
var _result_value1;
|
|
throw Object.defineProperty(new Error(`Invariant: expected a page response, got ${(_result_value1 = result.value) == null ? void 0 : _result_value1.kind}`), "__NEXT_ERROR_CODE", {
|
|
value: "E305",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
// 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();
|
|
}
|
|
});
|
|
}
|
|
|
|
//# sourceMappingURL=app-page.js.map
|