Repository: vercel/next.js
Pull request: Apply normalizers for init URL used for handlers

This applies our normalizers to the `initURL` we pass to the handlers interface as currently it is created before our existing normalizing takes place which causes us to fail to generate the correct cache keys for ISR paths. This specifically breaks our de-duping for client segment cache as it relies on the cache key being consistent across all segment paths being requested during a revalidation. 

In the future we will have to hoist the URL normalizing anyways so this leaves it as is until then. 

This also adds a regression test for the cache key not being normalized causing extra render passes for client segment cache entries when it shouldn't have. 

Changed files:
filename: packages/next/src/server/base-server.ts
patch: @@ -2637,7 +2637,18 @@ export default abstract class Server<
             const parsedInitUrl = parseUrl(
               getRequestMeta(req, 'initURL') || req.url
             )
-            request.url = `${parsedInitUrl.pathname?.replace(/(\.prefetch\.rsc|\.rsc)$/, '')}${parsedInitUrl.search || ''}`
+            let initPathname = parsedInitUrl.pathname || '/'
+
+            for (const normalizer of [
+              this.normalizers.segmentPrefetchRSC,
+              this.normalizers.prefetchRSC,
+              this.normalizers.rsc,
+            ]) {
+              if (normalizer?.match(initPathname)) {
+                initPathname = normalizer.normalize(initPathname)
+              }
+            }
+            request.url = `${initPathname}${parsedInitUrl.search || ''}`
 
             // propagate the request context for dev
             setRequestMeta(request, getRequestMeta(req))

filename: test/production/standalone-mode/required-server-files/app/isr/[slug]/page.js
patch: @@ -5,6 +5,8 @@ export function generateStaticParams() {
 }
 
 export default async function Page({ params }) {
+  console.log('rendering /isr/[slug]')
+
   const data = await fetch(
     'https://next-data-api-endpoint.vercel.app/api/random',
     {

filename: test/production/standalone-mode/required-server-files/required-server-files-ppr.test.ts
patch: @@ -19,6 +19,7 @@ describe('required server files app router', () => {
   let appPort: number | string
   let delayedPostpone
   let rewritePostpone
+  let cliOutput = ''
 
   const setupNext = async ({
     nextEnv,
@@ -101,6 +102,12 @@ describe('required server files app router', () => {
       undefined,
       {
         cwd: next.testDir,
+        onStderr(data) {
+          cliOutput += data
+        },
+        onStdout(data) {
+          cliOutput += data
+        },
       }
     )
   }
@@ -118,6 +125,38 @@ describe('required server files app router', () => {
     expect(next.cliOutput).not.toContain('ERR_INVALID_URL')
   })
 
+  // this enables client segment cache in CI
+  if (process.env.__NEXT_EXPERIMENTAL_PPR) {
+    it('should de-dupe client segment tree revalidate requests', async () => {
+      const { segmentPaths } = await next.readJSON(
+        'standalone/.next/server/app/isr/first.meta'
+      )
+      const outputIdx = cliOutput.length
+
+      for (const segmentPath of segmentPaths) {
+        const outputSegmentPath =
+          join('/isr/[slug].segments', segmentPath) + '.segment.rsc'
+
+        require('console').error('requesting', outputSegmentPath)
+
+        const res = await fetchViaHTTP(appPort, outputSegmentPath, undefined, {
+          headers: {
+            'x-matched-path': '/isr/[slug].segments/_tree.segment.rsc',
+            'x-now-route-matches': 'slug=first&1=first',
+          },
+        })
+
+        expect(res.status).toBe(200)
+        expect(res.headers.get('content-type')).toBe('text/x-component')
+      }
+
+      expect(
+        cliOutput.substring(outputIdx).match(/rendering \/isr\/\[slug\]/g)
+          .length
+      ).toBe(1)
+    })
+  }
+
   it('should properly stream resume with Next-Resume', async () => {
     const res = await fetchViaHTTP(appPort, '/delayed', undefined, {
       headers: {