diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index b7041e32c0049..378924ddd53a5 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -2145,6 +2145,13 @@ export default abstract class Server< const isDynamicRSCRequest = isRoutePPREnabled && isRSCRequest && !isPrefetchRSCRequest + // 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 = + req.headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER.toLowerCase()] + // we need to ensure the status code if /404 is visited directly if (is404Page && !isNextDataRequest && !isRSCRequest) { res.statusCode = 404 @@ -2755,6 +2762,7 @@ export default abstract class Server< rscData: metadata.flightData, postponed: metadata.postponed, status: res.statusCode, + segmentData: undefined, } satisfies CachedAppPageValue, revalidate: metadata.revalidate, isFallback: !!fallbackRouteParams, @@ -3090,6 +3098,51 @@ export default abstract class Server< } ) + if ( + isRoutePPREnabled && + isPrefetchRSCRequest && + typeof segmentPrefetchHeader === 'string' + ) { + if (cacheEntry?.value?.kind === CachedRouteKind.APP_PAGE) { + // This is a prefetch request for an individual segment's static data. + // Unless the segment is fully dynamic, the data should have already been + // loaded into the cache, when the page itself was generated. So we should + // always either return the cache entry. If no cache entry is available, + // it's a 404 — either the segment is fully dynamic, or an invalid segment + // path was requested. + if (cacheEntry.value.segmentData) { + const matchedSegment = + cacheEntry.value.segmentData[segmentPrefetchHeader] + if (matchedSegment !== undefined) { + return { + type: 'rsc', + body: RenderResult.fromStatic(matchedSegment), + // TODO: Eventually this should use revalidate time of the + // individual segment, not the whole page. + revalidate: cacheEntry.revalidate, + } + } + } + // If the segment is not found, return a 404. Since this is an RSC + // request, there's no reason to render a 404 page; just return an + // empty response. + res.statusCode = 404 + return { + type: 'rsc', + body: RenderResult.fromStatic(''), + revalidate: cacheEntry.revalidate, + } + } else { + // Segment prefetches should never reach the application layer. If + // there's no cache entry for this page, it's a 404. + res.statusCode = 404 + return { + type: 'rsc', + body: RenderResult.fromStatic(''), + } + } + } + if (isPreviewMode) { res.setHeader( 'Cache-Control', diff --git a/packages/next/src/server/lib/incremental-cache/file-system-cache.ts b/packages/next/src/server/lib/incremental-cache/file-system-cache.ts index ee359deb05d2b..d9bc76db35542 100644 --- a/packages/next/src/server/lib/incremental-cache/file-system-cache.ts +++ b/packages/next/src/server/lib/incremental-cache/file-system-cache.ts @@ -14,6 +14,8 @@ import { NEXT_DATA_SUFFIX, NEXT_META_SUFFIX, RSC_PREFETCH_SUFFIX, + RSC_SEGMENT_SUFFIX, + RSC_SEGMENTS_DIR_SUFFIX, RSC_SUFFIX, } from '../../../lib/constants' @@ -263,6 +265,38 @@ export default class FileSystemCache implements CacheHandler { ) } catch {} + let maybeSegmentData: { [segmentPath: string]: string } | undefined + if (meta?.segmentPaths) { + // Collect all the segment data for this page. + // TODO: To optimize file system reads, we should consider creating + // separate cache entries for each segment, rather than storing them + // all on the page's entry. Though the behavior is + // identical regardless. + const segmentData: { [segmentPath: string]: string } = {} + maybeSegmentData = segmentData + const segmentsDir = key + RSC_SEGMENTS_DIR_SUFFIX + await Promise.all( + meta.segmentPaths.map(async (segmentPath: string) => { + const segmentDataFilePath = this.getFilePath( + segmentPath === '/' + ? segmentsDir + '/_index' + RSC_SEGMENT_SUFFIX + : segmentsDir + segmentPath + RSC_SEGMENT_SUFFIX, + IncrementalCacheKind.APP_PAGE + ) + try { + segmentData[segmentPath] = await this.fs.readFile( + segmentDataFilePath, + 'utf8' + ) + } catch { + // This shouldn't happen, but if for some reason we fail to + // load a segment from the filesystem, treat it the same as if + // the segment is dynamic and does not have a prefetch. + } + }) + ) + } + let rscData: Buffer | undefined if (!isFallback) { rscData = await this.fs.readFile( @@ -282,6 +316,7 @@ export default class FileSystemCache implements CacheHandler { postponed: meta?.postponed, headers: meta?.headers, status: meta?.status, + segmentData: maybeSegmentData, }, } } else if (kind === IncrementalCacheKind.PAGES) { @@ -405,6 +440,7 @@ export default class FileSystemCache implements CacheHandler { headers: data.headers, status: data.status, postponed: undefined, + segmentPaths: undefined, } await this.fs.writeFile( @@ -447,6 +483,7 @@ export default class FileSystemCache implements CacheHandler { headers: data.headers, status: data.status, postponed: data.postponed, + segmentPaths: undefined, } await this.fs.writeFile( diff --git a/packages/next/src/server/response-cache/types.ts b/packages/next/src/server/response-cache/types.ts index e81671424644d..0ea4afd789249 100644 --- a/packages/next/src/server/response-cache/types.ts +++ b/packages/next/src/server/response-cache/types.ts @@ -77,6 +77,7 @@ export interface CachedAppPageValue { status: number | undefined postponed: string | undefined headers: OutgoingHttpHeaders | undefined + segmentData: { [segmentPath: string]: string } | undefined } export interface CachedPageValue { @@ -117,6 +118,7 @@ export interface IncrementalCachedAppPageValue { headers: OutgoingHttpHeaders | undefined postponed: string | undefined status: number | undefined + segmentData: { [segmentPath: string]: string } | undefined } export interface IncrementalCachedPageValue { diff --git a/packages/next/src/server/response-cache/utils.ts b/packages/next/src/server/response-cache/utils.ts index d34d03d22d9e8..7e2bedf60251d 100644 --- a/packages/next/src/server/response-cache/utils.ts +++ b/packages/next/src/server/response-cache/utils.ts @@ -32,6 +32,7 @@ export async function fromResponseCacheEntry( rscData: cacheEntry.value.rscData, headers: cacheEntry.value.headers, status: cacheEntry.value.status, + segmentData: cacheEntry.value.segmentData, } : cacheEntry.value, } @@ -70,6 +71,7 @@ export async function toResponseCacheEntry( headers: response.value.headers, status: response.value.status, postponed: response.value.postponed, + segmentData: response.value.segmentData, } satisfies CachedAppPageValue) : response.value, }