diff --git a/packages/next/src/server/lib/router-utils/resolve-routes.ts b/packages/next/src/server/lib/router-utils/resolve-routes.ts index f4bc247c9ed5cf..c74f0968ff3890 100644 --- a/packages/next/src/server/lib/router-utils/resolve-routes.ts +++ b/packages/next/src/server/lib/router-utils/resolve-routes.ts @@ -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, + } } } diff --git a/test/e2e/app-dir/app-middleware/app-middleware.test.ts b/test/e2e/app-dir/app-middleware/app-middleware.test.ts index a33a344769b9be..e5a113e593ec18 100644 --- a/test/e2e/app-dir/app-middleware/app-middleware.test.ts +++ b/test/e2e/app-dir/app-middleware/app-middleware.test.ts @@ -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', () => { diff --git a/test/e2e/app-dir/app-middleware/middleware.js b/test/e2e/app-dir/app-middleware/middleware.js index ccb4785577e253..e341734febb4bb 100644 --- a/test/e2e/app-dir/app-middleware/middleware.js +++ b/test/e2e/app-dir/app-middleware/middleware.js @@ -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,