Repository: vercel/next.js
Pull request: Fix prefetch inlining build failure with dynamic routes

When prefetchInlining is enabled, the segment data collection returns a special key for the inlined response. Previously this key was `/_inlined`, but the build validation code in `packages/next/src/build/index.ts` expects segment paths to contain `__PAGE__`. This caused builds to fail with "Invariant: missing __PAGE__ segmentPath" for any dynamic route using prefetchInlining.

Fix by using the same `PAGE_SEGMENT_KEY` (`__PAGE__`) constant for the inlined segment response key, matching what the build code already expects. This is consistent since the inlined response serves the same role as the per-page segment data — it just bundles all segments together.

Closes #90850

Co-authored-by: Ben Salmon <ben.salmon@daylightgroup.nz>

Changed files:
filename: packages/next/src/client/components/segment-cache/cache.ts
patch: @@ -2015,7 +2015,7 @@ export async function fetchInlinedSegmentsOnCacheMiss(
   const headers: RequestHeaders = {
     [RSC_HEADER]: '1',
     [NEXT_ROUTER_PREFETCH_HEADER]: '1',
-    [NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]: '/_inlined',
+    [NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]: '/' + PAGE_SEGMENT_KEY,
   }
   if (nextUrl !== null) {
     headers[NEXT_URL] = nextUrl

filename: packages/next/src/server/app-render/collect-segment-data.tsx
patch: @@ -7,6 +7,7 @@ import type {
   HeadData,
 } from '../../shared/lib/app-router-types'
 import { readVaryParams } from '../../shared/lib/segment-cache/vary-params-decoding'
+import { PAGE_SEGMENT_KEY } from '../../shared/lib/segment'
 import type { ManifestNode } from '../../build/webpack/plugins/flight-manifest-plugin'
 
 // eslint-disable-next-line import/no-extraneous-dependencies
@@ -540,7 +541,7 @@ async function renderInlinedPrefetchResponse(
     onError: onSegmentPrerenderError,
   })
   const buffer = await streamToBuffer(prelude)
-  return ['/_inlined' as SegmentRequestKey, buffer]
+  return [('/' + PAGE_SEGMENT_KEY) as SegmentRequestKey, buffer]
 }
 
 async function buildInlinedSegmentPrefetch(

filename: test/e2e/app-dir/segment-cache/prefetch-inlining/app/dynamic/[slug]/page.tsx
patch: @@ -0,0 +1,18 @@
+import { Suspense } from 'react'
+
+async function Content({ params }: { params: Promise<{ slug: string }> }) {
+  const { slug } = await params
+  return <p id="dynamic-page">{slug}</p>
+}
+
+export default function DynamicPage({
+  params,
+}: {
+  params: Promise<{ slug: string }>
+}) {
+  return (
+    <Suspense fallback={<p>Loading...</p>}>
+      <Content params={params} />
+    </Suspense>
+  )
+}

filename: test/e2e/app-dir/segment-cache/prefetch-inlining/app/page.tsx
patch: @@ -16,6 +16,9 @@ export default function Home() {
             Route A (duplicate)
           </LinkAccordion>
         </li>
+        <li>
+          <LinkAccordion href="/dynamic/hello">Dynamic Route</LinkAccordion>
+        </li>
       </ul>
     </div>
   )

filename: test/e2e/app-dir/segment-cache/prefetch-inlining/prefetch-inlining.test.ts
patch: @@ -16,9 +16,9 @@ describe('prefetch inlining', () => {
     //   /shared/a/b/c  and  /shared/a/d/e
     // Without inlining, prefetching each route would issue one request per
     // segment plus one for the head (6+ requests). With inlining enabled,
-    // all segment data is bundled into a single /_inlined response, so
-    // revealing a link should produce at most 2 prefetch requests per route:
-    // one for /_tree and one for /_inlined.
+    // all segment data is bundled into a single response, so revealing a
+    // link should produce at most 2 prefetch requests per route: one for
+    // /_tree and one for the inlined segment data.
 
     let rscRequestCount = 0
     let page: Playwright.Page
@@ -78,6 +78,14 @@ describe('prefetch inlining', () => {
     expect(text).toBe('Page E')
   })
 
+  it('works with dynamic routes', async () => {
+    // Regression test: the build previously failed with
+    // "Invariant: missing __PAGE__ segmentPath" when prefetchInlining was
+    // combined with dynamic routes.
+    const $ = await next.render$('/dynamic/hello')
+    expect($('#dynamic-page').text()).toBe('hello')
+  })
+
   it('deduplicates inlined prefetch requests for the same route', async () => {
     // Two links point to the same route (/shared/a/b/c). When the first
     // link is revealed, its prefetch spawns an inlined request. While