Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions packages/next/src/server/lib/router-utils/resolve-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
}
}
}

Expand Down
25 changes: 24 additions & 1 deletion test/e2e/app-dir/app-middleware/app-middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})

Expand Down Expand Up @@ -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', () => {
Expand Down
11 changes: 11 additions & 0 deletions test/e2e/app-dir/app-middleware/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading