Repository: vercel/next.js
Pull request: [test] Fix deploy test of `cache-components.server-action.test.ts`

When deployed, the sentinels where showing `at runtime` instead of `at buildtime`. There are two reasons for this:

1. The page sentinel was accidentally rendered in a client component, which causes a hydration mismatch, and the value toggling from `at buildtime` to `at runtime` when the page is hydrated before the assertion is done.
2. The middleware was setting sentinel cookies that are intended for the `/cookies/*` pages, but not for the `/server-action-inline` page. This caused the server action (which is triggered in the second test) to consider the page as revalidated, which then led the client router to refetch the page. With `next start` this is not a problem because the RSC response is prerendered at build time, and returned as a static file, which contains the correct sentinel values. However, when deployed on Vercel, the RSC request results in an ISR cache miss for some reason, which causes the page to be revalidated (with runtime sentinel values), and then the third test fails when it receives the regenerated response instead of the build-time prerender result. This needs to be investigated separately, but for now we disable the middleware from setting cookies on this page to prevent the ISR revalidation request.

closes #88817
closes NAR-740

> [!TIP]
> Best reviewed with hidden whitespace changes.

Changed files:
filename: test/e2e/app-dir/cache-components/app/server-action-inline/form.tsx
patch: @@ -1,7 +1,6 @@
 'use client'
 
 import { ReactNode, useActionState } from 'react'
-import { getSentinelValue } from '../getSentinelValue'
 
 export function Form({ action }: { action: () => Promise<ReactNode> }) {
   const [result, formAction] = useActionState(action, 'initial')
@@ -11,7 +10,6 @@ export function Form({ action }: { action: () => Promise<ReactNode> }) {
       <h1>Inline Server Action with Cache Components</h1>
       <button>Submit</button>
       <p>{result}</p>
-      <div id="page">{getSentinelValue()}</div>
     </form>
   )
 }

filename: test/e2e/app-dir/cache-components/app/server-action-inline/page.tsx
patch: @@ -1,4 +1,5 @@
 import { Form } from './form'
+import { getSentinelValue } from '../getSentinelValue'
 
 export default function Page() {
   const simpleValue = 'result'
@@ -7,16 +8,19 @@ export default function Page() {
   // Async components emit timing chunks
   const timedValue = <HasTimingInfo />
   return (
-    <Form
-      action={async () => {
-        'use server'
-        return (
-          <>
-            {simpleValue} {jsxValue} {timedValue}
-          </>
-        )
-      }}
-    />
+    <>
+      <Form
+        action={async () => {
+          'use server'
+          return (
+            <>
+              {simpleValue} {jsxValue} {timedValue}
+            </>
+          )
+        }}
+      />
+      <div id="page">{getSentinelValue()}</div>
+    </>
   )
 }
 

filename: test/e2e/app-dir/cache-components/middleware.ts
patch: @@ -2,43 +2,56 @@ import type { NextRequest } from 'next/server'
 import { NextResponse } from 'next/server'
 
 export function middleware(request: NextRequest) {
-  // Clone the request headers and set a new header `x-hello-from-middleware1`
-  const requestHeaders = new Headers(request.headers)
-  requestHeaders.set('x-sentinel', 'hello')
-  requestHeaders.set('x-sentinel-path', request.nextUrl.pathname)
-  requestHeaders.set(
-    'x-sentinel-rand',
-    ((Math.random() * 100000) | 0).toString(16)
-  )
+  if (
+    request.nextUrl.pathname.startsWith('/headers/') ||
+    request.nextUrl.pathname.endsWith('dynamic_api_headers')
+  ) {
+    // Clone the request headers and set a new header `x-hello-from-middleware1`
+    const requestHeaders = new Headers(request.headers)
+    requestHeaders.set('x-sentinel', 'hello')
+    requestHeaders.set('x-sentinel-path', request.nextUrl.pathname)
+    requestHeaders.set(
+      'x-sentinel-rand',
+      ((Math.random() * 100000) | 0).toString(16)
+    )
 
-  const response = NextResponse.next({
-    request: {
-      // New request headers
-      headers: requestHeaders,
-    },
-  })
-  response.cookies.set('x-sentinel', 'hello', {
-    httpOnly: true,
-    sameSite: 'strict',
-    maxAge: 60 * 60 * 24 * 7, // 1 week
-    path: '/',
-  })
-  response.cookies.set('x-sentinel-path', request.nextUrl.pathname, {
-    httpOnly: true,
-    sameSite: 'strict',
-    maxAge: 60 * 60 * 24 * 7, // 1 week
-    path: '/',
-  })
-  response.cookies.set(
-    'x-sentinel-rand',
-    ((Math.random() * 100000) | 0).toString(16),
-    {
+    return NextResponse.next({
+      request: {
+        // New request headers
+        headers: requestHeaders,
+      },
+    })
+  }
+
+  const response = NextResponse.next()
+
+  if (
+    request.nextUrl.pathname.startsWith('/cookies/') ||
+    request.nextUrl.pathname.endsWith('dynamic_api_cookies')
+  ) {
+    response.cookies.set('x-sentinel', 'hello', {
+      httpOnly: true,
+      sameSite: 'strict',
+      maxAge: 60 * 60 * 24 * 7, // 1 week
+      path: '/',
+    })
+    response.cookies.set('x-sentinel-path', request.nextUrl.pathname, {
       httpOnly: true,
       sameSite: 'strict',
       maxAge: 60 * 60 * 24 * 7, // 1 week
       path: '/',
-    }
-  )
+    })
+    response.cookies.set(
+      'x-sentinel-rand',
+      ((Math.random() * 100000) | 0).toString(16),
+      {
+        httpOnly: true,
+        sameSite: 'strict',
+        maxAge: 60 * 60 * 24 * 7, // 1 week
+        path: '/',
+      }
+    )
+  }
 
   return response
 }