rsnext/packages/next/server/web/adapter.ts
JJ Kasper 122899bd37
Add hard navigation guard and fix middleware rewrite cases (#37815)
This adds a guard for whenever we do a hard navigation over a client-navigation to ensure we aren't redirecting to the same URL that we are currently on as this can cause infinite redirecting. This also fixes some cases with middleware rewrites without i18n enabled and expands our middleware suite to test both with i18n and without. 

This also fixes a race condition with the query updating where a user could attempt a route transition and it then gets overridden by the query updating and prevents firing router events during the query updating as these can be false signals of a transition.

## Bug

- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

Fixes: https://github.com/vercel/next.js/issues/37804
2022-06-20 11:31:19 +00:00

188 lines
5.5 KiB
TypeScript

import type { NextMiddleware, RequestData, FetchEventResult } from './types'
import type { RequestInit } from './spec-extension/request'
import { PageSignatureError } from './error'
import { fromNodeHeaders } from './utils'
import { NextFetchEvent } from './spec-extension/fetch-event'
import { NextRequest } from './spec-extension/request'
import { NextResponse } from './spec-extension/response'
import { relativizeURL } from '../../shared/lib/router/utils/relativize-url'
import { waitUntilSymbol } from './spec-extension/fetch-event'
import { NextURL } from './next-url'
export async function adapter(params: {
handler: NextMiddleware
page: string
request: RequestData
}): Promise<FetchEventResult> {
const requestUrl = new NextURL(params.request.url, {
headers: params.request.headers,
nextConfig: params.request.nextConfig,
})
// Ensure users only see page requests, never data requests.
const buildId = requestUrl.buildId
requestUrl.buildId = ''
const isDataReq = params.request.headers['x-nextjs-data']
if (isDataReq && requestUrl.pathname === '/index') {
requestUrl.pathname = '/'
}
// clean-up any internal query params
for (const key of [...requestUrl.searchParams.keys()]) {
if (key.startsWith('__next')) {
requestUrl.searchParams.delete(key)
}
}
const request = new NextRequestHint({
page: params.page,
input: String(requestUrl),
init: {
body: params.request.body,
geo: params.request.geo,
headers: fromNodeHeaders(params.request.headers),
ip: params.request.ip,
method: params.request.method,
nextConfig: params.request.nextConfig,
},
})
/**
* This allows to identify the request as a data request. The user doesn't
* need to know about this property neither use it. We add it for testing
* purposes.
*/
if (isDataReq) {
Object.defineProperty(request, '__isData', {
enumerable: false,
value: true,
})
}
const event = new NextFetchEvent({ request, page: params.page })
let response = await params.handler(request, event)
/**
* For rewrites we must always include the locale in the final pathname
* so we re-create the NextURL forcing it to include it when the it is
* an internal rewrite. Also we make sure the outgoing rewrite URL is
* a data URL if the request was a data request.
*/
const rewrite = response?.headers.get('x-middleware-rewrite')
if (response && rewrite) {
const rewriteUrl = new NextURL(rewrite, {
forceLocale: true,
headers: params.request.headers,
nextConfig: params.request.nextConfig,
})
if (rewriteUrl.host === request.nextUrl.host) {
rewriteUrl.buildId = buildId || rewriteUrl.buildId
response.headers.set('x-middleware-rewrite', String(rewriteUrl))
}
/**
* When the request is a data request we must show if there was a rewrite
* with an internal header so the client knows which component to load
* from the data request.
*/
if (isDataReq) {
response.headers.set(
'x-nextjs-rewrite',
relativizeURL(String(rewriteUrl), String(requestUrl))
)
}
}
/**
* For redirects we will not include the locale in case when it is the
* default and we must also make sure the outgoing URL is a data one if
* the incoming request was a data request.
*/
const redirect = response?.headers.get('Location')
if (response && redirect) {
const redirectURL = new NextURL(redirect, {
forceLocale: false,
headers: params.request.headers,
nextConfig: params.request.nextConfig,
})
/**
* Responses created from redirects have immutable headers so we have
* to clone the response to be able to modify it.
*/
response = new Response(response.body, response)
if (redirectURL.host === request.nextUrl.host) {
redirectURL.buildId = buildId || redirectURL.buildId
response.headers.set('Location', String(redirectURL))
}
/**
* When the request is a data request we can't use the location header as
* it may end up with CORS error. Instead we map to an internal header so
* the client knows the destination.
*/
if (isDataReq) {
response.headers.delete('Location')
response.headers.set(
'x-nextjs-redirect',
relativizeURL(String(redirectURL), String(requestUrl))
)
}
}
return {
response: response || NextResponse.next(),
waitUntil: Promise.all(event[waitUntilSymbol]),
}
}
export function blockUnallowedResponse(
promise: Promise<FetchEventResult>
): Promise<FetchEventResult> {
return promise.then((result) => {
if (result.response?.body) {
console.error(
new Error(
`A middleware can not alter response's body. Learn more: https://nextjs.org/docs/messages/returning-response-body-in-middleware`
)
)
return {
...result,
response: new Response('Internal Server Error', {
status: 500,
statusText: 'Internal Server Error',
}),
}
}
return result
})
}
class NextRequestHint extends NextRequest {
sourcePage: string
constructor(params: {
init: RequestInit
input: Request | string
page: string
}) {
super(params.input, params.init)
this.sourcePage = params.page
}
get request() {
throw new PageSignatureError({ page: this.sourcePage })
}
respondWith() {
throw new PageSignatureError({ page: this.sourcePage })
}
waitUntil() {
throw new PageSignatureError({ page: this.sourcePage })
}
}