Repository: vercel/next.js
Pull request: backport: 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 {
+  getRedirectStatus,
+  allowedStatusCodes,
+} from '../../../lib/redirect-status'
 import { normalizeRepeatedSlashes } from '../../../shared/lib/utils'
 import { getRelativeURL } from '../../../shared/lib/router/utils/relativize-url'
 import { addPathPrefix } from '../../../shared/lib/router/utils/add-path-prefix'
@@ -657,15 +660,36 @@ export function getResolveRoutes(
 
             if (middlewareHeaders['location']) {
               const value = middlewareHeaders['location'] as string
-              const rel = getRelativeURL(value, initUrl)
-              resHeaders['location'] = rel
-              parsedUrl = url.parse(rel, true)
 
-              return {
-                parsedUrl,
-                resHeaders,
-                finished: true,
-                statusCode: middlewareRes.status,
+              // 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
+              )
+
+              if (isRedirectStatus) {
+                // Process as redirect: update parsedUrl and convert to relative URL
+                const rel = getRelativeURL(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: @@ -6,7 +6,7 @@ import { nextTestSetup, FileRef } from 'e2e-utils'
 import type { Response } from 'node-fetch'
 
 describe('app-dir with middleware', () => {
-  const { next } = nextTestSetup({
+  const { next, isNextDeploy } = nextTestSetup({
     files: __dirname,
   })
 
@@ -248,6 +248,29 @@ describe('app-dir with middleware', () => {
 
     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'
+      )
+    })
+  }
 })
 
 describe('app dir - middleware without pages dir', () => {

filename: test/e2e/app-dir/app-middleware/middleware.js
patch: @@ -78,6 +78,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,