Repository: vercel/next.js
Pull request: Discard prerendered route handler data from FS cache after revalidation

After having revalidated a prerendered route handler, on the subsequent request we must discard the prerendered data from the file system cache, as we already do for pages and fetch caches as well.

> [!NOTE]  
> This PR is best reviewed with hidden whitespace changes.

Changed files:
filename: packages/next/src/server/lib/incremental-cache/file-system-cache.ts
patch: @@ -101,8 +101,8 @@ export default class FileSystemCache implements CacheHandler {
 
     // let's check the disk for seed data
     if (!data && process.env.NEXT_RUNTIME !== 'edge') {
-      if (kind === IncrementalCacheKind.APP_ROUTE) {
-        try {
+      try {
+        if (kind === IncrementalCacheKind.APP_ROUTE) {
           const filePath = this.getFilePath(
             `${key}.body`,
             IncrementalCacheKind.APP_ROUTE
@@ -117,7 +117,7 @@ export default class FileSystemCache implements CacheHandler {
             )
           )
 
-          const cacheEntry: CacheHandlerValue = {
+          data = {
             lastModified: mtime.getTime(),
             value: {
               kind: CachedRouteKind.APP_ROUTE,
@@ -126,146 +126,141 @@ export default class FileSystemCache implements CacheHandler {
               status: meta.status,
             },
           }
-          return cacheEntry
-        } catch {
-          return null
-        }
-      }
-
-      try {
-        const filePath = this.getFilePath(
-          kind === IncrementalCacheKind.FETCH ? key : `${key}.html`,
-          kind
-        )
+        } else {
+          const filePath = this.getFilePath(
+            kind === IncrementalCacheKind.FETCH ? key : `${key}.html`,
+            kind
+          )
 
-        const fileData = await this.fs.readFile(filePath, 'utf8')
-        const { mtime } = await this.fs.stat(filePath)
+          const fileData = await this.fs.readFile(filePath, 'utf8')
+          const { mtime } = await this.fs.stat(filePath)
 
-        if (kind === IncrementalCacheKind.FETCH) {
-          const { tags, fetchIdx, fetchUrl } = ctx
+          if (kind === IncrementalCacheKind.FETCH) {
+            const { tags, fetchIdx, fetchUrl } = ctx
 
-          if (!this.flushToDisk) return null
+            if (!this.flushToDisk) return null
 
-          const lastModified = mtime.getTime()
-          const parsedData: CachedFetchValue = JSON.parse(fileData)
-          data = {
-            lastModified,
-            value: parsedData,
-          }
+            const lastModified = mtime.getTime()
+            const parsedData: CachedFetchValue = JSON.parse(fileData)
+            data = {
+              lastModified,
+              value: parsedData,
+            }
 
-          if (data.value?.kind === CachedRouteKind.FETCH) {
-            const storedTags = data.value?.tags
+            if (data.value?.kind === CachedRouteKind.FETCH) {
+              const storedTags = data.value?.tags
 
-            // update stored tags if a new one is being added
-            // TODO: remove this when we can send the tags
-            // via header on GET same as SET
-            if (!tags?.every((tag) => storedTags?.includes(tag))) {
-              if (FileSystemCache.debug) {
-                console.log('tags vs storedTags mismatch', tags, storedTags)
+              // update stored tags if a new one is being added
+              // TODO: remove this when we can send the tags
+              // via header on GET same as SET
+              if (!tags?.every((tag) => storedTags?.includes(tag))) {
+                if (FileSystemCache.debug) {
+                  console.log('tags vs storedTags mismatch', tags, storedTags)
+                }
+                await this.set(key, data.value, {
+                  fetchCache: true,
+                  tags,
+                  fetchIdx,
+                  fetchUrl,
+                })
               }
-              await this.set(key, data.value, {
-                fetchCache: true,
-                tags,
-                fetchIdx,
-                fetchUrl,
-              })
             }
-          }
-        } else if (kind === IncrementalCacheKind.APP_PAGE) {
-          // We try to load the metadata file, but if it fails, we don't
-          // error. We also don't load it if this is a fallback.
-          let meta: RouteMetadata | undefined
-          try {
-            meta = JSON.parse(
-              await this.fs.readFile(
-                filePath.replace(/\.html$/, NEXT_META_SUFFIX),
-                'utf8'
-              )
-            )
-          } catch {}
-
-          let maybeSegmentData: Map<string, Buffer> | 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: Map<string, Buffer> = new Map()
-            maybeSegmentData = segmentData
-            const segmentsDir = key + RSC_SEGMENTS_DIR_SUFFIX
-            await Promise.all(
-              meta.segmentPaths.map(async (segmentPath: string) => {
-                const segmentDataFilePath = this.getFilePath(
-                  segmentsDir + segmentPath + RSC_SEGMENT_SUFFIX,
-                  IncrementalCacheKind.APP_PAGE
+          } else if (kind === IncrementalCacheKind.APP_PAGE) {
+            // We try to load the metadata file, but if it fails, we don't
+            // error. We also don't load it if this is a fallback.
+            let meta: RouteMetadata | undefined
+            try {
+              meta = JSON.parse(
+                await this.fs.readFile(
+                  filePath.replace(/\.html$/, NEXT_META_SUFFIX),
+                  'utf8'
                 )
-                try {
-                  segmentData.set(
-                    segmentPath,
-                    await this.fs.readFile(segmentDataFilePath)
+              )
+            } catch {}
+
+            let maybeSegmentData: Map<string, Buffer> | 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: Map<string, Buffer> = new Map()
+              maybeSegmentData = segmentData
+              const segmentsDir = key + RSC_SEGMENTS_DIR_SUFFIX
+              await Promise.all(
+                meta.segmentPaths.map(async (segmentPath: string) => {
+                  const segmentDataFilePath = this.getFilePath(
+                    segmentsDir + segmentPath + RSC_SEGMENT_SUFFIX,
+                    IncrementalCacheKind.APP_PAGE
                   )
-                } 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 (!ctx.isFallback) {
-            rscData = await this.fs.readFile(
-              this.getFilePath(
-                `${key}${ctx.isRoutePPREnabled ? RSC_PREFETCH_SUFFIX : RSC_SUFFIX}`,
-                IncrementalCacheKind.APP_PAGE
+                  try {
+                    segmentData.set(
+                      segmentPath,
+                      await this.fs.readFile(segmentDataFilePath)
+                    )
+                  } 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.
+                  }
+                })
               )
-            )
-          }
-
-          data = {
-            lastModified: mtime.getTime(),
-            value: {
-              kind: CachedRouteKind.APP_PAGE,
-              html: fileData,
-              rscData,
-              postponed: meta?.postponed,
-              headers: meta?.headers,
-              status: meta?.status,
-              segmentData: maybeSegmentData,
-            },
-          }
-        } else if (kind === IncrementalCacheKind.PAGES) {
-          let meta: RouteMetadata | undefined
-          let pageData: string | object = {}
+            }
 
-          if (!ctx.isFallback) {
-            pageData = JSON.parse(
-              await this.fs.readFile(
+            let rscData: Buffer | undefined
+            if (!ctx.isFallback) {
+              rscData = await this.fs.readFile(
                 this.getFilePath(
-                  `${key}${NEXT_DATA_SUFFIX}`,
-                  IncrementalCacheKind.PAGES
-                ),
-                'utf8'
+                  `${key}${ctx.isRoutePPREnabled ? RSC_PREFETCH_SUFFIX : RSC_SUFFIX}`,
+                  IncrementalCacheKind.APP_PAGE
+                )
               )
-            )
-          }
+            }
 
-          data = {
-            lastModified: mtime.getTime(),
-            value: {
-              kind: CachedRouteKind.PAGES,
-              html: fileData,
-              pageData,
-              headers: meta?.headers,
-              status: meta?.status,
-            },
+            data = {
+              lastModified: mtime.getTime(),
+              value: {
+                kind: CachedRouteKind.APP_PAGE,
+                html: fileData,
+                rscData,
+                postponed: meta?.postponed,
+                headers: meta?.headers,
+                status: meta?.status,
+                segmentData: maybeSegmentData,
+              },
+            }
+          } else if (kind === IncrementalCacheKind.PAGES) {
+            let meta: RouteMetadata | undefined
+            let pageData: string | object = {}
+
+            if (!ctx.isFallback) {
+              pageData = JSON.parse(
+                await this.fs.readFile(
+                  this.getFilePath(
+                    `${key}${NEXT_DATA_SUFFIX}`,
+                    IncrementalCacheKind.PAGES
+                  ),
+                  'utf8'
+                )
+              )
+            }
+
+            data = {
+              lastModified: mtime.getTime(),
+              value: {
+                kind: CachedRouteKind.PAGES,
+                html: fileData,
+                pageData,
+                headers: meta?.headers,
+                status: meta?.status,
+              },
+            }
+          } else {
+            throw new Error(
+              `Invariant: Unexpected route kind ${kind} in file system cache.`
+            )
           }
-        } else {
-          throw new Error(
-            `Invariant: Unexpected route kind ${kind} in file system cache.`
-          )
         }
 
         if (data) {
@@ -278,6 +273,7 @@ export default class FileSystemCache implements CacheHandler {
 
     if (
       data?.value?.kind === CachedRouteKind.APP_PAGE ||
+      data?.value?.kind === CachedRouteKind.APP_ROUTE ||
       data?.value?.kind === CachedRouteKind.PAGES
     ) {
       let cacheTags: undefined | string[]

filename: test/cache-components-tests-manifest.json
patch: @@ -28,10 +28,7 @@
       ]
     },
     "test/e2e/app-dir/use-cache/use-cache.test.ts": {
-      "failed": [
-        "use-cache should revalidate before redirecting in a route handler",
-        "use-cache should update after unstable_expireTag correctly"
-      ]
+      "failed": ["use-cache should update after unstable_expireTag correctly"]
     },
     "test/production/app-dir/browser-chunks/browser-chunks.test.ts": {
       "failed": [