Repository: vercel/next.js
Pull request: fix: app-page URL normalization

## Summary

Fix an App Router request normalization gap where app-page route-module requests could keep internal RSC/segment-prefetch URLs in `req.url`.

This could leak paths like:

- `.../.segments/_tree.segment.rsc`
- `.../__PAGE__.segment.rsc`

into App Router render state and surface through `usePathname()` during server rendering.

## What changed

- Normalize `req.url` in the app-page route-module path after `.rsc` and `.segments/*.segment.rsc` pathname normalization.

## Why

`base-server` already keeps `req.url` in sync after RSC normalization ([ref](https://github.com/vercel/next.js/blob/5e11e67759fa38fe67f70790d7bba8c9ff9ba5fd/packages/next/src/server/base-server.ts#L676-L680)), but the app-page route-module path only updated `parsedUrl.pathname`.

Since App Router render state seeds its canonical URL from `req.url`, that desync could cause internal segment-prefetch paths to be treated as the current pathname.

Changed files:
filename: packages/next/src/server/route-modules/app-page/module.ts
patch: @@ -32,6 +32,7 @@ import { RSCPathnameNormalizer } from '../../normalizers/request/rsc'
 import { SegmentPrefixRSCPathnameNormalizer } from '../../normalizers/request/segment-prefix-rsc'
 import type { UrlWithParsedQuery } from 'url'
 import type { IncomingMessage } from 'http'
+import { normalizeAppPageRequestUrl } from './normalize-request-url'
 
 let vendoredReactRSC
 let vendoredReactSSR
@@ -142,6 +143,8 @@ export class AppPageRouteModule extends RouteModule<
     } else {
       super.normalizeUrl(req, parsedUrl)
     }
+
+    normalizeAppPageRequestUrl(req, parsedUrl.pathname || '/')
   }
 
   public render(

filename: packages/next/src/server/route-modules/app-page/normalize-request-url.test.ts
patch: @@ -0,0 +1,81 @@
+import {
+  NEXT_ROUTER_PREFETCH_HEADER,
+  NEXT_ROUTER_SEGMENT_PREFETCH_HEADER,
+  RSC_HEADER,
+} from '../../../client/components/app-router-headers'
+import { parseReqUrl } from '../../../lib/url'
+import { RSCPathnameNormalizer } from '../../normalizers/request/rsc'
+import { SegmentPrefixRSCPathnameNormalizer } from '../../normalizers/request/segment-prefix-rsc'
+import { normalizeAppPageRequestUrl } from './normalize-request-url'
+
+type TestRequest = {
+  headers: Record<string, string>
+  url: string
+}
+
+describe('normalizeAppPageRequestUrl', () => {
+  it('rewrites req.url for regular RSC requests', () => {
+    const req: TestRequest = {
+      headers: {},
+      url: '/docs/frameworks/frontend.rsc?foo=bar',
+    }
+    const parsedUrl = parseReqUrl(req.url)!
+
+    parsedUrl.pathname = new RSCPathnameNormalizer().normalize(
+      parsedUrl.pathname!
+    )
+    req.headers[RSC_HEADER] = '1'
+
+    normalizeAppPageRequestUrl(req, parsedUrl.pathname!)
+
+    expect(parsedUrl.pathname).toBe('/docs/frameworks/frontend')
+    expect(req.url).toBe('/docs/frameworks/frontend?foo=bar')
+    expect(req.headers[RSC_HEADER]).toBe('1')
+  })
+
+  it('rewrites req.url for segment prefetch RSC requests', () => {
+    const req: TestRequest = {
+      headers: {},
+      url: '/docs/frameworks/frontend.segments/_tree.segment.rsc?foo=bar',
+    }
+    const parsedUrl = parseReqUrl(req.url)!
+    const result = new SegmentPrefixRSCPathnameNormalizer().extract(
+      parsedUrl.pathname!
+    )!
+
+    parsedUrl.pathname = result.originalPathname
+    req.headers[RSC_HEADER] = '1'
+    req.headers[NEXT_ROUTER_PREFETCH_HEADER] = '1'
+    req.headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER] = result.segmentPath
+
+    normalizeAppPageRequestUrl(req, parsedUrl.pathname)
+
+    expect(parsedUrl.pathname).toBe('/docs/frameworks/frontend')
+    expect(req.url).toBe('/docs/frameworks/frontend?foo=bar')
+    expect(req.headers[RSC_HEADER]).toBe('1')
+    expect(req.headers[NEXT_ROUTER_PREFETCH_HEADER]).toBe('1')
+    expect(req.headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]).toBe('/_tree')
+  })
+
+  it('rewrites req.url for leaf page segment prefetch requests', () => {
+    const req: TestRequest = {
+      headers: {},
+      url: '/docs/frameworks/frontend.segments/docs/frameworks/frontend/__PAGE__.segment.rsc?foo=bar',
+    }
+    const parsedUrl = parseReqUrl(req.url)!
+    const result = new SegmentPrefixRSCPathnameNormalizer().extract(
+      parsedUrl.pathname!
+    )!
+
+    parsedUrl.pathname = result.originalPathname
+    req.headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER] = result.segmentPath
+
+    normalizeAppPageRequestUrl(req, parsedUrl.pathname)
+
+    expect(parsedUrl.pathname).toBe('/docs/frameworks/frontend')
+    expect(req.url).toBe('/docs/frameworks/frontend?foo=bar')
+    expect(req.headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]).toBe(
+      '/docs/frameworks/frontend/__PAGE__'
+    )
+  })
+})

filename: packages/next/src/server/route-modules/app-page/normalize-request-url.ts
patch: @@ -0,0 +1,22 @@
+import type { IncomingMessage } from 'http'
+
+import type { BaseNextRequest } from '../../base-http'
+import { parseReqUrl } from '../../../lib/url'
+import { formatUrl } from '../../../shared/lib/router/utils/format-url'
+
+export function normalizeAppPageRequestUrl(
+  req: Pick<IncomingMessage | BaseNextRequest, 'url'>,
+  pathname: string
+) {
+  if (!req.url) {
+    return
+  }
+
+  const normalizedUrl = parseReqUrl(req.url)
+  if (!normalizedUrl) {
+    return
+  }
+
+  normalizedUrl.pathname = pathname
+  req.url = formatUrl(normalizedUrl)
+}