Repository: vercel/next.js
Pull request: Do not modify parsedUrl collecting params

This updates to not modify the parsed URL values when collecting rewrite params inside of the `RouteModule`. This avoids invalid double rewrite cases one at the CDN layer and once again when collecting rewrite params inside the function. 

In the future we should have a de-opt to skip the in function rewrite param parsing when a user has properly configured their CDN to handle their rewrites and pass them down to the function. 

x-ref: [slack thread](https://vercel.slack.com/archives/C04M81FAWE6/p1755806771519109?thread_ts=1755668503.049659&cid=C04M81FAWE6)

Changed files:
filename: packages/next/src/server/route-modules/pages/pages-handler.ts
patch: @@ -116,6 +116,7 @@ export const getHandler = ({
       routerServerContext,
       nextConfig,
       resolvedPathname,
+      encodedResolvedPathname,
     } = prepareResult
 
     const isExperimentalCompile =
@@ -197,8 +198,8 @@ export const getHandler = ({
 
       const resolvedUrl = formatUrl({
         pathname: nextConfig.trailingSlash
-          ? parsedUrl.pathname
-          : removeTrailingSlash(parsedUrl.pathname || '/'),
+          ? `${encodedResolvedPathname}${!encodedResolvedPathname.endsWith('/') && parsedUrl.pathname?.endsWith('/') ? '/' : ''}`
+          : removeTrailingSlash(encodedResolvedPathname || '/'),
         // make sure to only add query values from original URL
         query: hasStaticProps ? {} : originalQuery,
       })

filename: packages/next/src/server/route-modules/route-module.ts
patch: @@ -508,6 +508,7 @@ export abstract class RouteModule<
         pageIsDynamic: boolean
         isDraftMode: boolean
         resolvedPathname: string
+        encodedResolvedPathname: string
         isNextDataRequest: boolean
         buildManifest: DeepReadonly<BuildManifest>
         fallbackBuildManifest: DeepReadonly<BuildManifest>
@@ -624,9 +625,13 @@ export abstract class RouteModule<
     const locale =
       getRequestMeta(req, 'locale') || detectedLocale || defaultLocale
 
+    // we apply rewrites against cloned URL so that we don't
+    // modify the original with the rewrite destination
+    const clonedParsedUrl = structuredClone(parsedUrl)
     const rewriteParamKeys = Object.keys(
-      serverUtils.handleRewrites(req, parsedUrl)
+      serverUtils.handleRewrites(req, clonedParsedUrl)
     )
+    Object.assign(parsedUrl.query, clonedParsedUrl.query)
 
     // after processing rewrites we want to remove locale
     // from parsedUrl pathname
@@ -635,6 +640,11 @@ export abstract class RouteModule<
         parsedUrl.pathname || '/',
         i18n.locales
       ).pathname
+
+      clonedParsedUrl.pathname = normalizeLocalePath(
+        clonedParsedUrl.pathname || '/',
+        i18n.locales
+      ).pathname
     }
 
     let params: Record<string, undefined | string | string[]> | undefined =
@@ -643,7 +653,9 @@ export abstract class RouteModule<
     // attempt parsing from pathname
     if (!params && serverUtils.dynamicRouteMatcher) {
       const paramsMatch = serverUtils.dynamicRouteMatcher(
-        normalizeDataPath(localeResult?.pathname || parsedUrl.pathname || '/')
+        normalizeDataPath(
+          clonedParsedUrl?.pathname || parsedUrl.pathname || '/'
+        )
       )
       const paramsResult = serverUtils.normalizeDynamicRouteParams(
         paramsMatch || {},
@@ -809,6 +821,10 @@ export abstract class RouteModule<
     if (resolvedPathname === '/index') {
       resolvedPathname = '/'
     }
+    const encodedResolvedPathname = resolvedPathname
+
+    // we decode for cache key/manifest usage encoded is
+    // for URL building
     try {
       resolvedPathname = decodePathParams(resolvedPathname)
     } catch (_) {}
@@ -829,6 +845,7 @@ export abstract class RouteModule<
       previewData,
       pageIsDynamic,
       resolvedPathname,
+      encodedResolvedPathname,
       isOnDemandRevalidate,
       revalidateOnlyGenerated,
       ...manifests,