Repository: vercel/next.js
Pull request: [backport v14]: fix router handling when setting a location response header (#82588)

Backports:
- #82588

Changed files:
filename: packages/next/src/server/lib/router-utils/resolve-routes.ts
patch: @@ -18,7 +18,10 @@ import { formatHostname } from '../format-hostname'
 import { toNodeOutgoingHttpHeaders } from '../../web/utils'
 import { isAbortError } from '../../pipe-readable'
 import { getHostname } from '../../../shared/lib/get-hostname'
-import { getRedirectStatus } from '../../../lib/redirect-status'
+import {
+  allowedStatusCodes,
+  getRedirectStatus,
+} from '../../../lib/redirect-status'
 import { normalizeRepeatedSlashes } from '../../../shared/lib/utils'
 import { relativizeURL } from '../../../shared/lib/router/utils/relativize-url'
 import { addPathPrefix } from '../../../shared/lib/router/utils/add-path-prefix'
@@ -640,15 +643,35 @@ export function getResolveRoutes(
 
             if (middlewareHeaders['location']) {
               const value = middlewareHeaders['location'] as string
-              const rel = relativizeURL(value, initUrl)
-              resHeaders['location'] = rel
-              parsedUrl = url.parse(rel, true)
+              // Only process Location header as a redirect if it has a proper redirect status
+              // This prevents a Location header with non-redirect status from being treated as a redirect
+              const isRedirectStatus = allowedStatusCodes.has(
+                middlewareRes.status
+              )
 
-              return {
-                parsedUrl,
-                resHeaders,
-                finished: true,
-                statusCode: middlewareRes.status,
+              if (isRedirectStatus) {
+                // Process as redirect: update parsedUrl and convert to relative URL
+                const rel = relativizeURL(value, initUrl)
+                resHeaders['location'] = rel
+                parsedUrl = url.parse(rel, true)
+
+                return {
+                  parsedUrl,
+                  resHeaders,
+                  finished: true,
+                  statusCode: middlewareRes.status,
+                }
+              } else {
+                // Not a redirect: just pass through the Location header
+                resHeaders['location'] = value
+
+                return {
+                  parsedUrl,
+                  resHeaders,
+                  finished: true,
+                  bodyStream,
+                  statusCode: middlewareRes.status,
+                }
               }
             }
 

filename: test/e2e/app-dir/app-middleware/app-middleware.test.ts
patch: @@ -11,7 +11,7 @@ createNextDescribe(
     files: __dirname,
     skipDeployment: true,
   },
-  ({ next }) => {
+  ({ next, isNextDeploy }) => {
     it('should filter correctly after middleware rewrite', async () => {
       const browser = await next.browser('/start')
 
@@ -240,6 +240,29 @@ createNextDescribe(
 
       await browser.deleteCookies()
     })
+
+    // TODO: This consistently 404s on Vercel deployments. It technically
+    // doesn't repro the bug we're trying to fix but we need to figure out
+    // why the handling is different.
+    if (!isNextDeploy) {
+      it('should not incorrectly treat a Location header as a rewrite', async () => {
+        const res = await next.fetch('/test-location-header')
+
+        // Should get status 200 (not a redirect status)
+        expect(res.status).toBe(200)
+
+        // Should get the JSON response associated with the route,
+        // and not follow the redirect
+        const json = await res.json()
+        expect(json).toEqual({ foo: 'bar' })
+
+        // Ensure the provided location is still on the response
+        const locationHeader = res.headers.get('location')
+        expect(locationHeader).toBe(
+          'https://next-data-api-endpoint.vercel.app/api/random'
+        )
+      })
+    }
   }
 )
 

filename: test/e2e/app-dir/app-middleware/middleware.js
patch: @@ -69,6 +69,17 @@ export async function middleware(request) {
     return res
   }
 
+  if (request.nextUrl.pathname === '/test-location-header') {
+    return NextResponse.json(
+      { foo: 'bar' },
+      {
+        headers: {
+          location: 'https://next-data-api-endpoint.vercel.app/api/random',
+        },
+      }
+    )
+  }
+
   return NextResponse.next({
     request: {
       headers: headersFromRequest,