Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -712,5 +712,7 @@
"711": "Can't resolve %s",
"712": "`rspack.warnForEdgeRuntime` is not supported by the wasm bindings.",
"713": "Unexpected error during process lookup",
"714": "cannot run loadNative when `NEXT_TEST_WASM` is set"
"714": "cannot run loadNative when `NEXT_TEST_WASM` is set",
"715": "expected a result to be returned",
"716": "expected a page response, got %s"
}
2 changes: 2 additions & 0 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ export type RoutesManifest = {
prefetchSegmentHeader: typeof NEXT_ROUTER_SEGMENT_PREFETCH_HEADER
prefetchSegmentDirSuffix: typeof RSC_SEGMENTS_DIR_SUFFIX
prefetchSegmentSuffix: typeof RSC_SEGMENT_SUFFIX
dynamicRSCPrerender: boolean
}
rewriteHeaders: {
pathHeader: typeof NEXT_REWRITTEN_PATH_HEADER
Expand Down Expand Up @@ -1336,6 +1337,7 @@ export default async function build(
prefetchSegmentHeader: NEXT_ROUTER_SEGMENT_PREFETCH_HEADER,
prefetchSegmentSuffix: RSC_SEGMENT_SUFFIX,
prefetchSegmentDirSuffix: RSC_SEGMENTS_DIR_SUFFIX,
dynamicRSCPrerender: isAppPPREnabled,
},
rewriteHeaders: {
pathHeader: NEXT_REWRITTEN_PATH_HEADER,
Expand Down
122 changes: 62 additions & 60 deletions packages/next/src/build/templates/app-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
import { getBotType, isBot } from '../../shared/lib/router/utils/is-bot'
import {
CachedRouteKind,
IncrementalCacheKind,
type CachedAppPageValue,
type CachedPageValue,
type ResponseCacheEntry,
Expand Down Expand Up @@ -126,7 +127,6 @@ export async function handler(
const multiZoneDraftMode = process.env
.__NEXT_MULTI_ZONE_DRAFT_MODE as any as boolean

const initialPostponed = getRequestMeta(req, 'postponed')
// TODO: replace with more specific flags
const minimalMode = getRequestMeta(req, 'minimalMode')

Expand Down Expand Up @@ -260,7 +260,9 @@ export async function handler(
// 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 ? initialPostponed : undefined
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
Expand Down Expand Up @@ -297,10 +299,10 @@ export async function handler(
!isSSG ||
// If this request has provided postponed data, it supports dynamic
// HTML.
typeof initialPostponed === 'string' ||
typeof minimalPostponed === 'string' ||
// If this is a dynamic RSC request, then this render supports dynamic
// HTML (it's dynamic).
isDynamicRSCRequest
(isDynamicRSCRequest && !minimalMode)

// When html bots request PPR page, perform the full dynamic rendering.
const shouldWaitOnAllReady = isHtmlBot && isRoutePPREnabled
Expand Down Expand Up @@ -415,6 +417,8 @@ export async function handler(
})
}

const incrementalCache = getRequestMeta(req, 'incrementalCache')

const doRender = async ({
span,
postponed,
Expand All @@ -432,6 +436,9 @@ export async function handler(
*/
fallbackRouteParams: FallbackRouteParams | null
}): Promise<ResponseCacheEntry> => {
// When we're resuming a render, we should allow dynamic response.
if (typeof postponed === 'string') supportsDynamicResponse = true

const context: AppPageRouteHandlerContext = {
query,
params,
Expand All @@ -457,8 +464,7 @@ export async function handler(
postponed,
shouldWaitOnAllReady,
serveStreamingMetadata,
supportsDynamicResponse:
typeof postponed === 'string' || supportsDynamicResponse,
supportsDynamicResponse,
buildManifest,
nextFontManifest,
reactLoadableManifest,
Expand Down Expand Up @@ -486,21 +492,11 @@ export async function handler(
reactMaxHeadersLength: nextConfig.reactMaxHeadersLength,

multiZoneDraftMode,
incrementalCache: getRequestMeta(req, 'incrementalCache'),
incrementalCache,
cacheLifeProfiles: nextConfig.experimental.cacheLife,
basePath: nextConfig.basePath,
serverActions: nextConfig.experimental.serverActions,

...(isDebugStaticShell || isDebugDynamicAccesses
? {
nextExport: true,
supportsDynamicResponse: false,
isStaticGeneration: true,
isRevalidate: true,
isDebugDynamicAccesses: isDebugDynamicAccesses,
}
: {}),

experimental: {
isRoutePPREnabled,
expireTime: nextConfig.expireTime,
Expand Down Expand Up @@ -534,6 +530,14 @@ export async function handler(
},
}

if (isDebugStaticShell || isDebugDynamicAccesses) {
context.renderOpts.nextExport = true
context.renderOpts.supportsDynamicResponse = false
context.renderOpts.isStaticGeneration = true
context.renderOpts.isRevalidate = true
context.renderOpts.isDebugDynamicAccesses = isDebugDynamicAccesses
}

const result = await invokeRouteModule(span, context)

const { metadata } = result
Expand Down Expand Up @@ -699,13 +703,42 @@ export async function handler(
}
}
}

// Only requests that aren't revalidating can be resumed. If we have the
// minimal postponed data, then we should resume the render with it.
const postponed =
let postponed =
!isOnDemandRevalidate && !isRevalidating && minimalPostponed
? minimalPostponed
: undefined

// If this is a dynamic RSC 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.
if (
process.env.NEXT_RUNTIME !== 'edge' &&
!minimalMode &&
incrementalCache &&
isDynamicRSCRequest
) {
const cachedEntry = await incrementalCache.get(resolvedPathname, {
kind: IncrementalCacheKind.APP_PAGE,
isRoutePPREnabled: true,
isFallback: false,
allowStale: true,
})

// If the cache entry is found, we should use the postponed data from
// the cache.
if (
cachedEntry &&
cachedEntry.value &&
cachedEntry.value.kind === CachedRouteKind.APP_PAGE
) {
postponed = cachedEntry.value.postponed
}
}

// 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 (
Expand Down Expand Up @@ -831,12 +864,7 @@ export async function handler(
// 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 (
minimalMode &&
isRSCRequest &&
!isPrefetchRSCRequest &&
isRoutePPREnabled
) {
else if (isDynamicRSCRequest) {
cacheControl = { revalidate: 0, expire: undefined }
} else if (!routeModule.isDev) {
// If this is a preview mode request, we shouldn't cache it
Expand Down Expand Up @@ -933,34 +961,15 @@ export async function handler(

// If there's a callback for `onCacheEntry`, call it with the cache entry
// and the revalidate options.
const onCacheEntry = getRequestMeta(req, 'onCacheEntry')
const onCacheEntry =
getRequestMeta(req, 'onCacheEntryV2') ??
// TODO: Remove this once we've migrated to `onCacheEntryV2`
getRequestMeta(req, 'onCacheEntry')
if (onCacheEntry) {
const finished = await onCacheEntry(
{
...cacheEntry,
// TODO: remove this when upstream doesn't
// always expect this value to be "PAGE"
value: {
...cacheEntry.value,
kind: 'PAGE',
},
},
{
url: getRequestMeta(req, 'initURL'),
}
)
if (finished) {
// TODO: maybe we have to end the request?
return null
}
}

// If the request has a postponed state and it's a resume request we
// should error.
if (didPostpone && minimalPostponed) {
throw new Error(
'Invariant: postponed state should not be present on a resume request'
)
const finished = await onCacheEntry(cacheEntry, {
url: getRequestMeta(req, 'initURL') ?? req.url,
})
if (finished) return null
}

if (cachedData.headers) {
Expand Down Expand Up @@ -1033,14 +1042,7 @@ export async function handler(
generateEtags: nextConfig.generateEtags,
poweredByHeader: nextConfig.poweredByHeader,
result: cachedData.html,
// Dynamic RSC responses cannot be cached, even if they're
// configured with `force-static` because we have no way of
// distinguishing between `force-static` and pages that have no
// postponed state.
// TODO: distinguish `force-static` from pages with no postponed state (static)
cacheControl: isDynamicRSCRequest
? { revalidate: 0, expire: undefined }
: cacheEntry.cacheControl,
cacheControl: cacheEntry.cacheControl,
})
}

Expand All @@ -1058,7 +1060,7 @@ export async function handler(
}

// This is a request for HTML data.
let body = cachedData.html
const body = cachedData.html

// 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
Expand Down
10 changes: 9 additions & 1 deletion packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1509,7 +1509,9 @@ async function renderToHTMLOrFlightImpl(
} else {
// We're rendering dynamically
const renderResumeDataCache =
renderOpts.renderResumeDataCache ?? postponedState?.renderResumeDataCache
renderOpts.renderResumeDataCache ??
postponedState?.renderResumeDataCache ??
null

const rootParams = getRootParams(loaderTree, ctx.getDynamicParamFromSegment)
const requestStore = createRequestStoreForRender(
Expand Down Expand Up @@ -1563,6 +1565,9 @@ async function renderToHTMLOrFlightImpl(

let formState: null | any = null
if (isPossibleActionRequest) {
// For action requests, we don't want to use the resume data cache.
requestStore.renderResumeDataCache = null

// For action requests, we handle them differently with a special render result.
const actionRequestResult = await handleAction({
req,
Expand Down Expand Up @@ -1603,6 +1608,9 @@ async function renderToHTMLOrFlightImpl(
}
}
}

// Restore the resume data cache
requestStore.renderResumeDataCache = renderResumeDataCache
}

const options: RenderResultOptions = {
Expand Down
6 changes: 3 additions & 3 deletions packages/next/src/server/async-storage/request-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export function createRequestStoreForRender(
previewProps: WrapperRenderOpts['previewProps'],
isHmrRefresh: RequestContext['isHmrRefresh'],
serverComponentsHmrCache: RequestContext['serverComponentsHmrCache'],
renderResumeDataCache: RenderResumeDataCache | undefined
renderResumeDataCache: RenderResumeDataCache | null
): RequestStore {
return createRequestStoreImpl(
// Pages start in render phase by default
Expand Down Expand Up @@ -148,7 +148,7 @@ export function createRequestStoreForAPI(
{},
implicitTags,
onUpdateCookies,
undefined,
null,
previewProps,
false,
undefined
Expand All @@ -163,7 +163,7 @@ function createRequestStoreImpl(
rootParams: Params,
implicitTags: RequestContext['implicitTags'],
onUpdateCookies: RenderOpts['onUpdateCookies'],
renderResumeDataCache: RenderResumeDataCache | undefined,
renderResumeDataCache: RenderResumeDataCache | null,
previewProps: WrapperRenderOpts['previewProps'],
isHmrRefresh: RequestContext['isHmrRefresh'],
serverComponentsHmrCache: RequestContext['serverComponentsHmrCache']
Expand Down
Loading