Repository: vercel/next.js
Pull request: Fix: Unresolved param in x-nextjs-rewritten-query

Fixes an issue with the x-nextjs-rewritten-query header where it responded without the dynamic parts of the URL filled in.

For example, when rewriting to `/hello?id=123` to `/hello/123`:
- Before: `x-nextjs-rewritten-query: id=__ESC_COLON_id`
- After: `x-nextjs-rewritten-query: id=123`

Changed files:
filename: packages/next/src/server/lib/router-utils/resolve-routes.ts
patch: @@ -33,7 +33,6 @@ import { addRequestMeta } from '../../request-meta'
 import {
   compileNonPath,
   matchHas,
-  parseDestination,
   prepareDestination,
 } from '../../../shared/lib/router/utils/prepare-destination'
 import type { TLSSocket } from 'tls'
@@ -761,16 +760,6 @@ export function getResolveRoutes(
             // so we'll just use the params from the route matcher
           }
 
-          // We extract the search params of the destination so we can set it on
-          // the response headers. We don't want to use the following
-          // `parsedDestination` as the query object is mutated.
-          const { search: destinationSearch, pathname: destinationPathname } =
-            parseDestination({
-              destination: route.destination,
-              params: rewriteParams,
-              query: parsedUrl.query,
-            })
-
           const { parsedDestination } = prepareDestination({
             appendParamsToQuery: true,
             destination: route.destination,
@@ -790,14 +779,17 @@ export function getResolveRoutes(
           if (req.headers[RSC_HEADER.toLowerCase()] === '1') {
             // We set the rewritten path and query headers on the response now
             // that we know that the it's not an external rewrite.
-            if (parsedUrl.pathname !== destinationPathname) {
-              res.setHeader(NEXT_REWRITTEN_PATH_HEADER, destinationPathname)
+            if (parsedUrl.pathname !== parsedDestination.pathname) {
+              res.setHeader(
+                NEXT_REWRITTEN_PATH_HEADER,
+                parsedDestination.pathname
+              )
             }
-            if (destinationSearch) {
+            if (parsedUrl.search !== parsedDestination.search) {
               res.setHeader(
                 NEXT_REWRITTEN_QUERY_HEADER,
                 // remove the leading ? from the search
-                destinationSearch.slice(1)
+                parsedDestination.search.slice(1)
               )
             }
           }

filename: packages/next/src/shared/lib/router/utils/prepare-destination.ts
patch: @@ -193,12 +193,18 @@ export function parseDestination(args: {
     hash = unescapeSegments(hash)
   }
 
+  let search = parsed.search
+  if (search) {
+    search = unescapeSegments(search)
+  }
+
   return {
     ...parsed,
     pathname,
     hostname,
     href,
     hash,
+    search,
   }
 }
 
@@ -210,7 +216,11 @@ export function prepareDestination(args: {
 }) {
   const parsedDestination = parseDestination(args)
 
-  const { hostname: destHostname, query: destQuery } = parsedDestination
+  const {
+    hostname: destHostname,
+    query: destQuery,
+    search: destSearch,
+  } = parsedDestination
 
   // The following code assumes that the pathname here includes the hash if it's
   // present.
@@ -311,7 +321,9 @@ export function prepareDestination(args: {
     }
     parsedDestination.pathname = pathname
     parsedDestination.hash = `${hash ? '#' : ''}${hash || ''}`
-    delete (parsedDestination as any).search
+    parsedDestination.search = destSearch
+      ? compileNonPath(destSearch, args.params)
+      : ''
   } catch (err: any) {
     if (err.message.match(/Expected .*? to not repeat, but got an array/)) {
       throw new Error(

filename: test/e2e/app-dir/rewrite-headers/next.config.js
patch: @@ -18,6 +18,10 @@ module.exports = {
         source: '/hello/(.*)/google',
         destination: 'https://www.google.$1/',
       },
+      {
+        source: '/rewrites-to-query/:name',
+        destination: '/other?key=:name',
+      },
     ]
   },
 }

filename: test/e2e/app-dir/rewrite-headers/rewrite-headers.test.ts
patch: @@ -368,6 +368,37 @@ const cases: {
       'x-nextjs-rewritten-query': 'key=value',
     },
   },
+  {
+    name: 'next.config.js rewrites with path-matched query HTML',
+    pathname: '/rewrites-to-query/andrew',
+    expected: {
+      'x-nextjs-rewritten-path': null,
+      'x-nextjs-rewritten-query': null,
+    },
+  },
+  {
+    name: 'next.config.js rewrites with path-matched query RSC',
+    pathname: '/rewrites-to-query/andrew',
+    headers: {
+      RSC: '1',
+    },
+    expected: {
+      'x-nextjs-rewritten-path': '/other',
+      'x-nextjs-rewritten-query': 'key=andrew',
+    },
+  },
+  {
+    name: 'next.config.js rewrites with path-matched query Prefetch RSC',
+    pathname: '/rewrites-to-query/andrew',
+    headers: {
+      RSC: '1',
+      'Next-Router-Prefetch': '1',
+    },
+    expected: {
+      'x-nextjs-rewritten-path': '/other',
+      'x-nextjs-rewritten-query': 'key=andrew',
+    },
+  },
 ]
 
 describe('rewrite-headers', () => {