Repository: vercel/next.js
Pull request: fix isDynamicRSC condition when deployed

Next.js relies on on the Resume Data Cache to prevent tearing between the statically prefetchable parts of the UI by seeding the resume with the same cache as the revalidation/build. The RDC data is stashed in the postponed state which is attached to all of the different outputs that comprise a page (the segments, the HTML, and the dynamic RSC data).

However, when deployed to targets like Vercel, the revalidation was incorrectly bypassing the cache for the RSC entry, because it thought that it was a resume. This led to a drift in cached values, because the revalidation would cause the main RSC entries RDC data to contain a different value than what was used for the segment prefetches & HTML ISR entry.

Fixes NAR-493 

Changed files:
filename: packages/next/src/build/templates/app-page.ts
patch: @@ -267,9 +267,16 @@ export async function handler(
   // 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 isDynamicRSCRequest =
+  let isDynamicRSCRequest =
     isRoutePPREnabled && isRSCRequest && !isPrefetchRSCRequest
 
+  // 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

filename: test/e2e/app-dir/resume-data-cache/app/revalidate/route.ts
patch: @@ -1,6 +1,6 @@
 import { revalidateTag } from 'next/cache'
 
 export function POST() {
-  revalidateTag('test', 'seconds')
+  revalidateTag('test', 'minutes')
   return new Response(null, { status: 200 })
 }

filename: test/e2e/app-dir/resume-data-cache/resume-data-cache.test.ts
patch: @@ -3,14 +3,11 @@ import { retry } from 'next-test-utils'
 import { computeCacheBustingSearchParam } from 'next/dist/shared/lib/router/utils/cache-busting-search-param'
 
 describe('resume-data-cache', () => {
-  const { next, isNextDeploy, isNextDev, skipped } = nextTestSetup({
+  const { next, isNextDev } = nextTestSetup({
     files: __dirname,
-    // TODO (rdc-for-navigations): This test is failing as the dynamic response
-    // is producing a different cached value than the static prefetch.
-    skipDeployment: true,
   })
 
-  if (isNextDev || skipped) {
+  if (isNextDev) {
     it('is skipped', () => {})
     return
   }
@@ -70,18 +67,14 @@ describe('resume-data-cache', () => {
       // same random number. The first request will get the stale data, but the
       // second request will get the fresh data as it'll eventually have
       // revalidated.
-      // NOTE: this current doesn't work on Next.js Deploy, as the dynamic RSC
-      // requests are not able to revalidate the page.
-      if (!isNextDeploy) {
-        const rsc = await next
-          .fetch('/', {
-            headers: {
-              RSC: '1',
-            },
-          })
-          .then((res) => res.text())
-        expect(rsc).toContain(first)
-      }
+      const rsc = await next
+        .fetch('/', {
+          headers: {
+            RSC: '1',
+          },
+        })
+        .then((res) => res.text())
+      expect(rsc).toContain(first)
 
       // We then expect after the background revalidation has been completed,
       // the dynamic RSC to get the fresh data.
@@ -113,6 +106,7 @@ describe('resume-data-cache', () => {
             headers: {
               RSC: '1',
               'Next-Router-Prefetch': '1',
+              'Next-Router-Segment-Prefetch': '/__PAGE__',
             },
           })
           .then((res) => res.text())