Repository: vercel/next.js
Pull request: fix router handling when setting a location response header

When middleware sets a `Location` header on the response, we should not assume that the target URL is the one we want to handle the request. We only want to route to the underlying location header when a rewrite is explicitly triggered rather than relying on the presence of the response header.

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'
@@ -659,15 +662,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,