Repository: vercel/next.js
Pull request: Fix pages API rewrite case

Fixes case failing in https://github.com/vercel/vercel/actions/runs/14716507754/job/41302495247?pr=13276 we were missing the default locale handling necessary to process rewrite params for i18n pages API routes. 

<details>

<summary>validated test case</summary>

![CleanShot 2025-04-28 at 16 05 57@2x](https://github.com/user-attachments/assets/a30e24fe-da95-4f27-a304-ed5e6a9fc6f6)

</details>

Changed files:
filename: packages/next/src/build/index.ts
patch: @@ -1321,6 +1321,11 @@ export default async function build(
               buildCustomRoute('redirect', r, restrictedRedirectPaths)
             ),
             headers: headers.map((r) => buildCustomRoute('header', r)),
+            rewrites: {
+              beforeFiles: [],
+              afterFiles: [],
+              fallback: [],
+            },
             dynamicRoutes,
             staticRoutes,
             dataRoutes: [],
@@ -1343,11 +1348,6 @@ export default async function build(
               pathHeader: NEXT_REWRITTEN_PATH_HEADER,
               queryHeader: NEXT_REWRITTEN_QUERY_HEADER,
             },
-            rewrites: {
-              beforeFiles: [],
-              afterFiles: [],
-              fallback: [],
-            },
             skipMiddlewareUrlNormalize: config.skipMiddlewareUrlNormalize,
             ppr: isAppPPREnabled
               ? {

filename: packages/next/src/build/templates/pages-api.ts
patch: @@ -28,8 +28,13 @@ import {
   routerServerGlobal,
 } from '../../server/lib/router-utils/router-server-context'
 import { removePathPrefix } from '../../shared/lib/router/utils/remove-path-prefix'
-import { normalizeLocalePath } from '../../shared/lib/i18n/normalize-locale-path'
+import {
+  normalizeLocalePath,
+  type PathLocale,
+} from '../../shared/lib/i18n/normalize-locale-path'
 import { loadManifestFromRelativePath } from '../../server/load-manifest.external'
+import { getHostname } from '../../shared/lib/get-hostname'
+import { detectDomainLocale } from '../../shared/lib/i18n/detect-domain-locale'
 
 // Re-export the handler (should be the default export).
 export default hoist(userland, 'default')
@@ -91,9 +96,11 @@ export async function handler(
     req.url = removePathPrefix(req.url || '/', basePath)
   }
 
+  let localeResult: PathLocale | undefined
+
   if (i18n) {
     const urlParts = (req.url || '/').split('?')
-    const localeResult = normalizeLocalePath(urlParts[0] || '/', i18n.locales)
+    localeResult = normalizeLocalePath(urlParts[0] || '/', i18n.locales)
 
     if (localeResult.detectedLocale) {
       req.url = `${localeResult.pathname}${
@@ -114,6 +121,21 @@ export async function handler(
     trailingSlash: process.env.__NEXT_TRAILING_SLASH as any as boolean,
     caseSensitive: Boolean(routesManifest.caseSensitive),
   })
+
+  const domainLocale = detectDomainLocale(
+    i18n?.domains,
+    getHostname(parsedUrl, req.headers),
+    localeResult?.detectedLocale
+  )
+
+  const defaultLocale = domainLocale?.defaultLocale || i18n?.defaultLocale
+
+  // Ensure parsedUrl.pathname includes locale before processing
+  // rewrites or they won't match correctly.
+  if (defaultLocale && !localeResult?.detectedLocale) {
+    parsedUrl.pathname = `/${defaultLocale}${parsedUrl.pathname}`
+  }
+
   const rewriteParamKeys = Object.keys(
     serverUtils.handleRewrites(req, parsedUrl)
   )
@@ -124,7 +146,9 @@ export async function handler(
 
   const params: Record<string, undefined | string | string[]> =
     serverUtils.dynamicRouteMatcher
-      ? serverUtils.dynamicRouteMatcher(parsedUrl.pathname || '') || {}
+      ? serverUtils.dynamicRouteMatcher(
+          localeResult?.pathname || parsedUrl.pathname || ''
+        ) || {}
       : {}
 
   const query = {

filename: packages/next/src/server/next-server.ts
patch: @@ -11,7 +11,7 @@ import {
 import type { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin'
 import type RenderResult from './render-result'
 import type { FetchEventResult } from './web/types'
-import type { PrerenderManifest } from '../build'
+import type { PrerenderManifest, RoutesManifest } from '../build'
 import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin'
 import type { NextParsedUrlQuery, NextUrlWithParsedQuery } from './request-meta'
 import type { Params } from './request/params'
@@ -1866,25 +1866,10 @@ export default class NextNodeServer extends BaseServer<
   }
 
   protected getRoutesManifest(): NormalizedRouteManifest | undefined {
-    return getTracer().trace(NextNodeServerSpan.getRoutesManifest, () => {
-      const manifest = loadManifest(join(this.distDir, ROUTES_MANIFEST)) as any
-
-      let rewrites = manifest.rewrites ?? {
-        beforeFiles: [],
-        afterFiles: [],
-        fallback: [],
-      }
-
-      if (Array.isArray(rewrites)) {
-        rewrites = {
-          beforeFiles: [],
-          afterFiles: rewrites,
-          fallback: [],
-        }
-      }
-
-      return { ...manifest, rewrites }
-    })
+    return getTracer().trace(
+      NextNodeServerSpan.getRoutesManifest,
+      () => loadManifest(join(this.distDir, ROUTES_MANIFEST)) as RoutesManifest
+    )
   }
 
   protected attachRequestMeta(