rsnext/packages/next/server/web/adapter.ts

268 lines
8 KiB
TypeScript
Raw Normal View History

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'
import { stripInternalSearchParams } from '../internal-utils'
import { normalizeRscPath } from '../../shared/lib/router/utils/app-paths'
import {
NEXT_ROUTER_PREFETCH,
NEXT_ROUTER_STATE_TREE,
RSC,
} from '../../client/components/app-router-headers'
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 })
}
}
const FLIGHT_PARAMETERS = [
[RSC],
[NEXT_ROUTER_STATE_TREE],
[NEXT_ROUTER_PREFETCH],
] as const
export async function adapter(params: {
handler: NextMiddleware
page: string
request: RequestData
}): Promise<FetchEventResult> {
// TODO-APP: use explicit marker for this
const isEdgeRendering = typeof self.__BUILD_MANIFEST !== 'undefined'
params.request.url = normalizeRscPath(params.request.url, true)
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 = '/'
}
const requestHeaders = fromNodeHeaders(params.request.headers)
// Parameters should only be stripped for middleware
if (!isEdgeRendering) {
for (const param of FLIGHT_PARAMETERS) {
requestHeaders.delete(param.toString().toLowerCase())
}
}
// Strip internal query parameters off the request.
stripInternalSearchParams(requestUrl.searchParams, true)
const request = new NextRequestHint({
page: params.page,
input: String(requestUrl),
init: {
body: params.request.body,
geo: params.request.geo,
headers: requestHeaders,
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)
// check if response is a Response object
if (response && !(response instanceof Response)) {
throw new TypeError('Expected an instance of Response to be returned')
}
/**
* 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 (!process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE) {
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 (!process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE) {
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 {
feat(middleware)!: forbids middleware response body (#36835) _Hello Next.js team! First PR here, I hope I've followed the right practices._ ### What's in there? It has been decided to only support the following uses cases in Next.js' middleware: - rewrite the URL (`x-middleware-rewrite` response header) - redirect to another URL (`Location` response header) - pass on to the next piece in the request pipeline (`x-middleware-next` response header) 1. during development, a warning on console tells developers when they are returning a response (either with `Response` or `NextResponse`). 2. at build time, this warning becomes an error. 3. at run time, returning a response body will trigger a 500 HTTP error with a JSON payload containing the detailed error. All returned/thrown errors contain a link to the documentation. This is a breaking feature compared to the _beta_ middleware implementation, and also removes `NextResponse.json()` which makes no sense any more. ### How to try it? - runtime behavior: `HEADLESS=true yarn jest test/integration/middleware/core` - build behavior : `yarn jest test/integration/middleware/build-errors` - development behavior: `HEADLESS=true yarn jest test/development/middleware-warnings` ### Notes to reviewers The limitation happens in next's web adapter. ~The initial implementation was to check `response.body` existence, but it turns out [`Response.redirect()`](https://github.com/vercel/next.js/blob/canary/packages/next/server/web/spec-compliant/response.ts#L42-L53) may set the response body (https://github.com/vercel/next.js/pull/31886). Hence why the proposed implementation specifically looks at response headers.~ `Response.redirect()` and `NextResponse.redirect()` do not need to include the final location in their body: it is handled by next server https://github.com/vercel/next.js/blob/canary/packages/next/server/next-server.ts#L1142 Because this is a breaking change, I had to adjust several tests cases, previously returning JSON/stream/text bodies. When relevant, these middlewares are returning data using response headers. About DevEx: relying on AST analysis to detect forbidden use cases is not as good as running the code. Such cases are easy to detect: ```js new Response('a text value') new Response(JSON.stringify({ /* whatever */ }) ``` But these are false-positive cases: ```js function returnNull() { return null } new Response(returnNull()) function doesNothing() {} new Response(doesNothing()) ``` However, I see no good reasons to let users ship middleware such as the one above, hence why the build will fail, even if _technically speaking_, they are not setting the response body. ## Feature - [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [x] Integration tests added - [x] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [x] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [x] Make sure the linting passes by running `yarn lint`
2022-05-20 00:02:20 +02:00
response: response || NextResponse.next(),
waitUntil: Promise.all(event[waitUntilSymbol]),
}
}
feat(middleware)!: forbids middleware response body (#36835) _Hello Next.js team! First PR here, I hope I've followed the right practices._ ### What's in there? It has been decided to only support the following uses cases in Next.js' middleware: - rewrite the URL (`x-middleware-rewrite` response header) - redirect to another URL (`Location` response header) - pass on to the next piece in the request pipeline (`x-middleware-next` response header) 1. during development, a warning on console tells developers when they are returning a response (either with `Response` or `NextResponse`). 2. at build time, this warning becomes an error. 3. at run time, returning a response body will trigger a 500 HTTP error with a JSON payload containing the detailed error. All returned/thrown errors contain a link to the documentation. This is a breaking feature compared to the _beta_ middleware implementation, and also removes `NextResponse.json()` which makes no sense any more. ### How to try it? - runtime behavior: `HEADLESS=true yarn jest test/integration/middleware/core` - build behavior : `yarn jest test/integration/middleware/build-errors` - development behavior: `HEADLESS=true yarn jest test/development/middleware-warnings` ### Notes to reviewers The limitation happens in next's web adapter. ~The initial implementation was to check `response.body` existence, but it turns out [`Response.redirect()`](https://github.com/vercel/next.js/blob/canary/packages/next/server/web/spec-compliant/response.ts#L42-L53) may set the response body (https://github.com/vercel/next.js/pull/31886). Hence why the proposed implementation specifically looks at response headers.~ `Response.redirect()` and `NextResponse.redirect()` do not need to include the final location in their body: it is handled by next server https://github.com/vercel/next.js/blob/canary/packages/next/server/next-server.ts#L1142 Because this is a breaking change, I had to adjust several tests cases, previously returning JSON/stream/text bodies. When relevant, these middlewares are returning data using response headers. About DevEx: relying on AST analysis to detect forbidden use cases is not as good as running the code. Such cases are easy to detect: ```js new Response('a text value') new Response(JSON.stringify({ /* whatever */ }) ``` But these are false-positive cases: ```js function returnNull() { return null } new Response(returnNull()) function doesNothing() {} new Response(doesNothing()) ``` However, I see no good reasons to let users ship middleware such as the one above, hence why the build will fail, even if _technically speaking_, they are not setting the response body. ## Feature - [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [x] Integration tests added - [x] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [x] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [x] Make sure the linting passes by running `yarn lint`
2022-05-20 00:02:20 +02:00
export function blockUnallowedResponse(
promise: Promise<FetchEventResult>
): Promise<FetchEventResult> {
if (process.env.__NEXT_ALLOW_MIDDLEWARE_RESPONSE_BODY) {
return promise
}
feat(middleware)!: forbids middleware response body (#36835) _Hello Next.js team! First PR here, I hope I've followed the right practices._ ### What's in there? It has been decided to only support the following uses cases in Next.js' middleware: - rewrite the URL (`x-middleware-rewrite` response header) - redirect to another URL (`Location` response header) - pass on to the next piece in the request pipeline (`x-middleware-next` response header) 1. during development, a warning on console tells developers when they are returning a response (either with `Response` or `NextResponse`). 2. at build time, this warning becomes an error. 3. at run time, returning a response body will trigger a 500 HTTP error with a JSON payload containing the detailed error. All returned/thrown errors contain a link to the documentation. This is a breaking feature compared to the _beta_ middleware implementation, and also removes `NextResponse.json()` which makes no sense any more. ### How to try it? - runtime behavior: `HEADLESS=true yarn jest test/integration/middleware/core` - build behavior : `yarn jest test/integration/middleware/build-errors` - development behavior: `HEADLESS=true yarn jest test/development/middleware-warnings` ### Notes to reviewers The limitation happens in next's web adapter. ~The initial implementation was to check `response.body` existence, but it turns out [`Response.redirect()`](https://github.com/vercel/next.js/blob/canary/packages/next/server/web/spec-compliant/response.ts#L42-L53) may set the response body (https://github.com/vercel/next.js/pull/31886). Hence why the proposed implementation specifically looks at response headers.~ `Response.redirect()` and `NextResponse.redirect()` do not need to include the final location in their body: it is handled by next server https://github.com/vercel/next.js/blob/canary/packages/next/server/next-server.ts#L1142 Because this is a breaking change, I had to adjust several tests cases, previously returning JSON/stream/text bodies. When relevant, these middlewares are returning data using response headers. About DevEx: relying on AST analysis to detect forbidden use cases is not as good as running the code. Such cases are easy to detect: ```js new Response('a text value') new Response(JSON.stringify({ /* whatever */ }) ``` But these are false-positive cases: ```js function returnNull() { return null } new Response(returnNull()) function doesNothing() {} new Response(doesNothing()) ``` However, I see no good reasons to let users ship middleware such as the one above, hence why the build will fail, even if _technically speaking_, they are not setting the response body. ## Feature - [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [x] Integration tests added - [x] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [x] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [x] Make sure the linting passes by running `yarn lint`
2022-05-20 00:02:20 +02:00
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
})
}
function getUnsupportedModuleErrorMessage(module: string) {
// warning: if you change these messages, you must adjust how react-dev-overlay's middleware detects modules not found
return `The edge runtime does not support Node.js '${module}' module.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime`
fix(edge-runtime): undefined global in edge runtime. (#38769) ## How to reproduce 1. create a next.js app with a middleware (or an edge route) that imports a node.js module: ```js // middleware.js import { NextResponse } from 'next/server' import { basename } from 'path' export default async function middleware() { basename() return NextResponse.next() } ``` 2. deploy it to vercel with `vc` 3. go to the your function logs in Vercel Front (https://vercel.com/$user/$project/$deployment/functions) 4. in another tab, query your application > it results in a 500 page: <img width="517" alt="image" src="https://user-images.githubusercontent.com/186268/179557102-72568ca9-bcfd-49e2-9b9c-c51c3064f2d7.png"> > in the logs you should see: <img width="1220" alt="image" src="https://user-images.githubusercontent.com/186268/179557266-498f3290-b7df-46ac-8816-7bb396821245.png"> ## Expected behavior The route should fail indeed in a 500, because Edge runtime **does not support node.js modules**. However the error in logs should be completely different: ```shell error - Error: The edge runtime does not support Node.js 'path' module. Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime ``` ## Notes to reviewers I introduced this issue in #38234. Prior to the PR above, the app would not even build, as we were checking imported node.js module during the build with AST analysis. Since #38234, the app would build and should fail at runtime, with an appropriate error. The mistake was to declare `__import_unsupported` function in the sandbox's context, that is only used in `next dev` and `next start`, but not shipped to Vercel platform. By loading it inside webpack loaders (both middleware and edge route), we ensure it will be defined on Vercel as well. The existing test suite (`pnpm testheadless --testPathPattern=runtime-module-error`) covers them.
2022-07-20 16:53:27 +02:00
}
function __import_unsupported(moduleName: string) {
const proxy: any = new Proxy(function () {}, {
get(_obj, prop) {
if (prop === 'then') {
return {}
}
throw new Error(getUnsupportedModuleErrorMessage(moduleName))
},
construct() {
throw new Error(getUnsupportedModuleErrorMessage(moduleName))
},
apply(_target, _this, args) {
if (typeof args[0] === 'function') {
return args[0](proxy)
}
throw new Error(getUnsupportedModuleErrorMessage(moduleName))
},
})
return new Proxy({}, { get: () => proxy })
}
export function enhanceGlobals() {
// The condition is true when the "process" module is provided
if (process !== global.process) {
// prefer local process but global.process has correct "env"
process.env = global.process.env
global.process = process
}
// to allow building code that import but does not use node.js modules,
// webpack will expect this function to exist in global scope
Object.defineProperty(globalThis, '__import_unsupported', {
value: __import_unsupported,
enumerable: false,
configurable: false,
})
fix(edge-runtime): undefined global in edge runtime. (#38769) ## How to reproduce 1. create a next.js app with a middleware (or an edge route) that imports a node.js module: ```js // middleware.js import { NextResponse } from 'next/server' import { basename } from 'path' export default async function middleware() { basename() return NextResponse.next() } ``` 2. deploy it to vercel with `vc` 3. go to the your function logs in Vercel Front (https://vercel.com/$user/$project/$deployment/functions) 4. in another tab, query your application > it results in a 500 page: <img width="517" alt="image" src="https://user-images.githubusercontent.com/186268/179557102-72568ca9-bcfd-49e2-9b9c-c51c3064f2d7.png"> > in the logs you should see: <img width="1220" alt="image" src="https://user-images.githubusercontent.com/186268/179557266-498f3290-b7df-46ac-8816-7bb396821245.png"> ## Expected behavior The route should fail indeed in a 500, because Edge runtime **does not support node.js modules**. However the error in logs should be completely different: ```shell error - Error: The edge runtime does not support Node.js 'path' module. Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime ``` ## Notes to reviewers I introduced this issue in #38234. Prior to the PR above, the app would not even build, as we were checking imported node.js module during the build with AST analysis. Since #38234, the app would build and should fail at runtime, with an appropriate error. The mistake was to declare `__import_unsupported` function in the sandbox's context, that is only used in `next dev` and `next start`, but not shipped to Vercel platform. By loading it inside webpack loaders (both middleware and edge route), we ensure it will be defined on Vercel as well. The existing test suite (`pnpm testheadless --testPathPattern=runtime-module-error`) covers them.
2022-07-20 16:53:27 +02:00
}