Relevant source files
The following files were used as context for generating this wiki page:
- packages/next/src/server/web/sandbox/sandbox.ts
- packages/next/src/build/templates/edge-ssr.ts
- packages/next/src/server/next-server.ts
- packages/next/src/build/templates/edge-ssr-app.ts
- packages/next/src/build/templates/edge-app-route.ts
- packages/next/src/server/web/sandbox/context.ts
- packages/next/src/server/web/adapter.ts
- packages/next/src/build/templates/middleware.ts
- packages/next/src/build/templates/app-route.ts
- packages/next/src/server/base-server.ts
- packages/next/src/server/dev/next-dev-server.ts
- packages/next/src/build/webpack/plugins/middleware-plugin.ts
- packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts
- packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts
- packages/next/src/build/templates/pages-edge-api.ts
- packages/next/src/server/web/edge-route-module-wrapper.ts
- packages/next/src/build/templates/pages-api.ts
- packages/next/src/server/route-modules/route-module.ts
- packages/next/src/build/templates/edge-wrapper.js
- packages/next/src/build/webpack/loaders/next-edge-function-loader.ts
- packages/next/src/build/webpack/loaders/next-middleware-wasm-loader.ts
- packages/next/src/server/web/internal-edge-wait-until.ts
- packages/next/src/build/webpack/loaders/next-middleware-asset-loader.ts
- packages/next/src/server/web/globals.ts
- packages/next/src/server/route-modules/pages-api/module.compiled.js
- packages/next/src/server/web/spec-extension/request.ts
- packages/next/src/server/web/spec-extension/response.ts
Overview
The Edge Runtime powers Next.js middleware, edge API routes, and edge-rendered App and Pages router components by executing userland code in a lightweight, web-standard V8 sandbox. Built on top of the edge-runtime package, the edge architecture establishes a secure global environment with process polyfills, stubs for unsupported Node.js APIs, and specialized Webpack loaders that package entrypoints and dependency manifests. Incoming HTTP requests and asynchronous fetch event lifecycles are normalized across the adapter layer, while bridge wrappers connect edge route modules to execution handlers, streaming responses, and waitUntil background task primitives.
Sources: packages/next/src/server/web/sandbox/sandbox.ts:1-44, packages/next/src/server/web/sandbox/context.ts:259-282, packages/next/src/build/webpack/plugins/middleware-plugin.ts:44-74, packages/next/src/server/web/adapter.ts:259-278, packages/next/src/server/web/edge-route-module-wrapper.ts:35-81
Edge Compilation and Manifest Packaging
Webpack Loaders for Edge Entrypoints
Next.js orchestrates the compilation and packaging of Edge Runtime components through specialized Webpack loaders and the MiddlewarePlugin. These components transform userland modules, collect module build metadata, manage asset and WebAssembly bindings, and emit the middleware.json manifest version 3.
Edge compilation relies on dedicated loaders that attach build information to webpack modules via getModuleBuildInfo. The EdgeAppRouteLoader processes App Router routes and page paths, configuring cache handlers and route metadata. Similarly, edgeSSRLoader handles Pages and App router Server-Side Rendering (SSR) entrypoints, swapping standard folder paths for their ESM equivalents (next/dist/esm/pages) to optimize tree-shaking and bundle sizing.
| Loader Name | Source File | Key Responsibilities |
|---|---|---|
EdgeAppRouteLoader | packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts | Configures nextEdgeSSR build info, parses base64 middleware configurations, and sets up cache handlers for App Router route modules. |
edgeSSRLoader | packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts | Resolves absolute paths for documents, error boundaries, and 500 pages while mapping dist folders to ESM variants. |
nextEdgeFunctionLoader | packages/next/src/build/webpack/loaders/next-edge-function-loader.ts | Injects global web polyfills, wraps page handlers using wrapApiHandler, and exports an adapter instance for Edge API routes. |
MiddlewareWasmLoader | packages/next/src/build/webpack/loaders/next-middleware-wasm-loader.ts | Computes a SHA-1 hash of binary WebAssembly files, emits chunks into server/edge-chunks/, and exposes module bindings. |
MiddlewareAssetLoader | packages/next/src/build/webpack/loaders/next-middleware-asset-loader.ts | Interpolates asset names with hashes, emits files to server/edge-chunks/, and exports a blob URL identifier string. |
Sources: packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts:57-117, packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts:104-228, packages/next/src/build/webpack/loaders/next-edge-function-loader.ts:15-71, packages/next/src/build/webpack/loaders/next-middleware-wasm-loader.ts:8-15, packages/next/src/build/webpack/loaders/next-middleware-asset-loader.ts:4-17
export default function MiddlewareWasmLoader(this: any, source: Buffer) {
const name = `wasm_${sha1(source)}`
const filePath = `edge-chunks/${name}.wasm`
const buildInfo = getModuleBuildInfo(this._module)
buildInfo.nextWasmMiddlewareBinding = { filePath: `server/${filePath}`, name }
this.emitFile(`/${filePath}`, source, null)
return `module.exports = ${name};`
}Sources: packages/next/src/build/webpack/loaders/next-middleware-wasm-loader.ts:8-15
MiddlewarePlugin Manifest Generation
The MiddlewarePlugin compiles execution metadata across all entrypoints to construct version 3 of the MiddlewareManifest. This manifest tracks sorted middleware patterns alongside defined middleware functions and edge SSR entrypoints, mapping each entrypoint to its generated file dependencies, environment variables, WebAssembly bindings, asset bindings, and target execution regions.
export interface EdgeFunctionDefinition {
files: string[]
name: string
page: string
entrypoint: string
matchers: ProxyMatcher[]
env: Record<string, string>
wasm?: AssetBinding[]
assets?: AssetBinding[]
regions?: string[] | string
}
export interface MiddlewareManifest {
version: 3
sortedMiddleware: string[]
middleware: { [page: string]: EdgeFunctionDefinition }
functions: { [page: string]: EdgeFunctionDefinition }
}Sources: packages/next/src/build/webpack/plugins/middleware-plugin.ts:44-64
Note
MiddlewarePlugin extracts file dependencies using getEntryFiles, which differentiates between server components requiring client reference manifests and standard edge functions, ensuring that Subresource Integrity (SRI) and dynamic CSS manifests are correctly bundled for edge SSR targets.
Sources: packages/next/src/build/webpack/plugins/middleware-plugin.ts:105-158
Global Environment and Runtime Polyfills
Environment Bootstrapping and Global Enhancement
When the runtime initialization checks process.env.NEXT_RUNTIME === 'edge', enhanceGlobals sets up the global environment. It synchronizes process.env with global.process.env and overwrites global.process to point to the sandboxed process instance. It also defines __import_unsupported on globalThis as a non-enumerable, non-configurable property. This helper returns a proxy that throws an error when property access, construction, or function invocation occurs on un-supported Node.js modules, enabling codebases to import modules that are unused at runtime without failing build checks. Additionally, it eagerly triggers ensureInstrumentationRegistered to initialize telemetry or instrumentation hooks.
function getUnsupportedModuleErrorMessage(module: string) {
// warning: if you change these messages, you must adjust how 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`
}
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 })
}
function enhanceGlobals() {
if (process.env.NEXT_RUNTIME !== 'edge') {
return
}
// 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
try {
Object.defineProperty(globalThis, '__import_unsupported', {
value: __import_unsupported,
enumerable: false,
configurable: false,
})
} catch {}
// Eagerly fire instrumentation hook to make the startup faster.
void ensureInstrumentationRegistered()
}Sources: packages/next/src/server/web/globals.ts:57-108
Warning
Accessing any property or calling a function on an unsupported Node.js module proxy created by __import_unsupported immediately throws an error stating that the edge runtime does not support that module.
Sources: packages/next/src/server/web/globals.ts:57-82
Runtime Context Extension and Node API Stubs
The module context creation function initializes an EdgeRuntime instance with code generation flags enabled when process.env.NODE_ENV !== 'production'. Through its extend hook, it attaches process polyfills, custom require implementations via NativeModuleMap, timers (setInterval, clearInterval, setTimeout, clearTimeout), performance hooks, and AsyncLocalStorage. It iterates over EDGE_UNSUPPORTED_NODE_APIS to add stubs for missing Node modules and attaches custom handlers for dynamic code evaluation warnings.
const runtime = new EdgeRuntime({
codeGeneration:
process.env.NODE_ENV !== 'production'
? { strings: true, wasm: true }
: undefined,
extend: (context) => {
context.process = createProcessPolyfill(edgeFunctionEntry.env)
Object.defineProperty(context, 'require', {
enumerable: false,
value: (id: string) => {
const value = NativeModuleMap.get(id)
if (!value) {
throw TypeError('Native module not found: ' + id)
}
return value
},
})
if (process.env.NODE_ENV !== 'production') {
context.__next_log_error__ = function (err: unknown) {
options.onError(err)
}
}
context.__next_eval__ = function __next_eval__(fn: Function) {
const key = fn.toString()
if (!warnedEvals.has(key)) {
const warning = getServerError(
new Error(
`Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime
Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`
),
COMPILER_NAMES.edgeServer
)
warning.name = 'DynamicCodeEvaluationWarning'
Error.captureStackTrace(warning, __next_eval__)
warnedEvals.add(key)
options.onWarning(warning)
}
return fn()
}
context.__next_webassembly_compile__ =
function __next_webassembly_compile__(fn: Function) {
const key = fn.toString()
if (!warnedWasmCodegens.has(key)) {
const warning = getServerError(
new Error(`Dynamic WASM code generation (e. g. 'WebAssembly.compile') not allowed in Edge Runtime.
Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`),
COMPILER_NAMES.edgeServer
)
warning.name = 'DynamicWasmCodeGenerationWarning'
Error.captureStackTrace(warning, __next_webassembly_compile__)
warnedWasmCodegens.add(key)
options.onWarning(warning)
}
return fn()
}
context.__next_webassembly_instantiate__ =
async function __next_webassembly_instantiate__(fn: Function) {
const result = await fn()
// If a buffer is given, WebAssembly.instantiate returns an object
// containing both a module and an instance while it returns only an
// instance if a WASM module is given. Utilize the fact to determine
// if the WASM code generation happens.
//
// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiate#primary_overload_%E2%80%94_taking_wasm_binary_code
const instantiatedFromBuffer = result.hasOwnProperty('module')
const key = fn.toString()
if (instantiatedFromBuffer && !warnedWasmCodegens.has(key)) {
const warning = getServerError(
new Error(`Dynamic WASM code generation ('WebAssembly.instantiate' with a buffer parameter) not allowed in Edge Runtime.
Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`),
COMPILER_NAMES.edgeServer
)
warning.name = 'DynamicWasmCodeGenerationWarning'Sources: packages/next/src/server/web/sandbox/context.ts:264-464
Note
Invoking eval or new Function triggers __next_eval__, which checks a warnedEvals set to emit a DynamicCodeEvaluationWarning once per unique function string in non-production environments.
Sources: packages/next/src/server/web/sandbox/context.ts:289-305
Development Sandbox Execution and Isolation
Module Context Creation and Global Injection
The createModuleContext function constructs an isolated EdgeRuntime environment for local development. It loads WebAssembly binaries from the edge function entry and configures code generation flags based on environment conditions. Within the runtime extension hook, it provisions custom globals including process polyfills, native module resolution via NativeModuleMap, timers, performance monitoring, and an overridden fetch implementation capable of resolving inline assets. It also intercepts Request constructor calls and Response.redirect to validate target URLs.
async function createModuleContext(options: ModuleContextOptions) {
const warnedEvals = new Set<string>()
const warnedWasmCodegens = new Set<string>()
const { edgeFunctionEntry } = options
const wasm = await loadWasm(edgeFunctionEntry.wasm ?? [])
const runtime = new EdgeRuntime({
codeGeneration:
process.env.NODE_ENV !== 'production'
? { strings: true, wasm: true }
: undefined,
extend: (context) => {
context.process = createProcessPolyfill(edgeFunctionEntry.env)
Object.defineProperty(context, 'require', {
enumerable: false,
value: (id: string) => {
const value = NativeModuleMap.get(id)
if (!value) {
throw TypeError('Native module not found: ' + id)
}
return value
},
})
if (process.env.NODE_ENV !== 'production') {
context.__next_log_error__ = function (err: unknown) {
options.onError(err)
}
}
context.__next_eval__ = function __next_eval__(fn: Function) {
const key = fn.toString()
if (!warnedEvals.has(key)) {
const warning = getServerError(
new Error(
`Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime
Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`
),
COMPILER_NAMES.edgeServer
)
warning.name = 'DynamicCodeEvaluationWarning'
Error.captureStackTrace(warning, __next_eval__)
warnedEvals.add(key)
options.onWarning(warning)
}
return fn()
}
context.__next_webassembly_compile__ =
function __next_webassembly_compile__(fn: Function) {
const key = fn.toString()
if (!warnedWasmCodegens.has(key)) {
const warning = getServerError(
new Error(`Dynamic WASM code generation (e. g. 'WebAssembly.compile') not allowed in Edge Runtime.
Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`),
COMPILER_NAMES.edgeServer
)
warning.name = 'DynamicWasmCodeGenerationWarning'
Error.captureStackTrace(warning, __next_webassembly_compile__)
warnedWasmCodegens.add(key)
options.onWarning(warning)
}
return fn()
}
context.__next_webassembly_instantiate__ =
async function __next_webassembly_instantiate__(fn: Function) {
const result = await fn()
// If a buffer is given, WebAssembly.instantiate returns an object
// containing both a module and an instance while it returns only an
// instance if a WASM module is given. Utilize the fact to determine
// if the WASM code generation happens.
//
// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiate#primary_overload_%E2%80%94_taking_wasm_binary_code
const instantiatedFromBuffer = result.hasOwnProperty('module')
const key = fn.toString()
if (instantiatedFromBuffer && !warnedWasmCodegens.has(key)) {
const warning = getServerError(Sources: packages/next/src/server/web/sandbox/context.ts:259-399
Warning
Attempting to instantiate WebAssembly modules from a buffer via WebAssembly.instantiate triggers __next_webassembly_instantiate__, emitting a DynamicWasmCodeGenerationWarning when detected outside precompiled modules.
Sources: packages/next/src/server/web/sandbox/context.ts:324-349
Runtime Context Evaluation and Runner Execution
The getRuntimeContext function initializes module contexts and injects shared global singletons into the sandbox globalThis object. These injections support incremental caching, router server contexts, server component HMR caching, and client asset tokens. Once injected, it evaluates all provided path entries within the context.
export async function getRuntimeContext(
params: Omit<RunnerFnParams, 'request'>
): Promise<EdgeRuntime<any>> {
const { runtime, evaluateInContext } = await getModuleContext({
moduleName: params.name,
onWarning: params.onWarning ?? (() => {}),
onError: params.onError ?? (() => {}),
useCache: params.useCache !== false,
edgeFunctionEntry: params.edgeFunctionEntry,
distDir: params.distDir,
})
if (params.incrementalCache) {
runtime.context.globalThis.__incrementalCacheShared = true
runtime.context.globalThis.__incrementalCache = params.incrementalCache
}
// expose router server context for access to dev handlers like
// logErrorWithOriginalStack
;(runtime.context.globalThis as any as typeof routerServerGlobal)[
RouterServerContextSymbol
] = routerServerGlobal[RouterServerContextSymbol]
if (params.serverComponentsHmrCache) {
runtime.context.globalThis.__serverComponentsHmrCache =
params.serverComponentsHmrCache
}
if (params.clientAssetToken) {
runtime.context.globalThis.NEXT_CLIENT_ASSET_SUFFIX =
params.clientAssetToken ? `?dpl=${params.clientAssetToken}` : ''
}
for (const paramPath of params.paths) {
evaluateInContext(paramPath)
}
return runtime
}Sources: packages/next/src/server/web/sandbox/sandbox.ts:72-109
The run function executes the compiled edge function handler within wrapped AsyncLocalStorage request stores and custom request contexts. It clones request bodies for non-GET/HEAD methods, builds standard headers while filtering forbidden headers, and finalizes request body streams upon completion.
export const run = withTaggedErrors(async function runWithTaggedErrors(params) {
const runtime = await getRuntimeContext(params)
const edgeFunction: EdgeHandler = (
await runtime.context._ENTRIES[`middleware_${params.name}`]
).default
const cloned = !['HEAD', 'GET'].includes(params.request.method)
? params.request.body?.cloneBodyStream()
: undefined
const KUint8Array = runtime.evaluate('Uint8Array')
const urlInstance = new URL(params.request.url)
params.request.url = urlInstance.toString()
const headers = new Headers()
for (const [key, value] of Object.entries(params.request.headers)) {
headers.set(key, value?.toString() ?? '')
}
try {
let result: FetchEventResult | undefined = undefined
const builtinRequestCtx: BuiltinRequestContextValue = {
...getBuiltinRequestContext(),
// FIXME(after):
// arguably, this is an abuse of "@next/request-context" --
// it'd make more sense to simply forward its existing value into the sandbox (in `createModuleContext`)
// but here we're using it to just pass in `waitUntil` regardless if we were running in this context or not.
waitUntil: params.request.waitUntil,
}
await edgeSandboxNextRequestContext.run(builtinRequestCtx, () =>
requestStore.run({ headers }, async () => {
result = await edgeFunction({
request: {
...params.request,
body:
cloned &&
requestToBodyStream(runtime.context, KUint8Array, cloned),
},
})
for (const headerName of FORBIDDEN_HEADERS) {
result.response.headers.delete(headerName)
}
})
)
if (!result) throw new Error('Edge function did not return a response')
return result
} finally {
await params.request.body?.finalize()
}
})Sources: packages/next/src/server/web/sandbox/sandbox.ts:111-163
Note
FORBIDDEN_HEADERS containing content-length, content-encoding, and transfer-encoding are unconditionally deleted from the resulting response headers before dispatch.
Sources: packages/next/src/server/web/sandbox/sandbox.ts:23-27, packages/next/src/server/web/sandbox/sandbox.ts:152-154
Error Tagging and Unhandled Event Handling
In development mode, withTaggedErrors decorates runner executions by passing caught errors and unhandled waitUntil rejections through getServerError with an 'edge-server' compiler tag, enabling original stack frames to be resolved and displayed correctly in development overlays.
function withTaggedErrors(fn: RunnerFn): RunnerFn {
if (process.env.NODE_ENV === 'development') {
const { getServerError } =
require('../../dev/node-stack-frames') as typeof import('../../dev/node-stack-frames')
return (params) =>
fn(params)
.then((result) => ({
...result,
waitUntil: result?.waitUntil?.catch((error) => {
// TODO: used COMPILER_NAMES.edgeServer instead. Verify that it does not increase the runtime size.
throw getServerError(error, 'edge-server')
}),
}))
.catch((error) => {
// TODO: used COMPILER_NAMES.edgeServer instead
throw getServerError(error, 'edge-server')
})
}
return fn
}Sources: packages/next/src/server/web/sandbox/sandbox.ts:49-70
Additionally, the runtime context binds event listeners for unhandled errors and rejections, using getDecorateUnhandledError and getDecorateUnhandledRejection to ensure runtime exceptions bubble up safely into the parent Node.js process with proper stack decorations.
const decorateUnhandledError = getDecorateUnhandledError(runtime)
runtime.context.addEventListener('error', decorateUnhandledError)
const decorateUnhandledRejection = getDecorateUnhandledRejection(runtime)
runtime.context.addEventListener(
'unhandledrejection',
decorateUnhandledRejection
)
Sources: packages/next/src/server/web/sandbox/context.ts:467-474
Edge Web Adapter Architecture
Request and Response Extensions
The adapter architecture bridges incoming Node.js requests and standard Web APIs through NextRequest and NextResponse, which extend the standard Request and Response interfaces with specialized Next.js behaviors. NextRequest parses incoming headers into structured RequestCookies via its internal symbol store and constructs a NextURL instance that handles internationalization, base paths, and query string normalization. When bodies are present in non-edge runtimes, NextRequest automatically sets the request duplex mode to 'half'.
export class NextRequest extends Request {
/** @internal */
[INTERNALS]: {
cookies: RequestCookies
url: string
nextUrl: NextURL
}
constructor(input: URL | RequestInfo, init: RequestInit = {}) {
const url =
typeof input !== 'string' && 'url' in input ? input.url : String(input)
validateURL(url)
// node Request instance requires duplex option when a body
// is present or it errors, we don't handle this for
// Request being passed in since it would have already
// errored if this wasn't configured
if (process.env.NEXT_RUNTIME !== 'edge') {
if (init.body && init.duplex !== 'half') {
init.duplex = 'half'
}
}
if (input instanceof Request) super(input, init)
else super(url, init)
const nextUrl = new NextURL(url, {
headers: toNodeOutgoingHttpHeaders(this.headers),
nextConfig: init.nextConfig,
})
this[INTERNALS] = {
cookies: new RequestCookies(this.headers),
nextUrl,
url: process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE
? url
: nextUrl.toString(),
}
}Sources: packages/next/src/server/web/spec-extension/request.ts:14-52
NextResponse wraps response cookies with a proxy that intercepts .set() and .delete() calls to automatically serialize cookie values and synchronize them into the 'x-middleware-set-cookie' header. It also exposes static helper methods for creating specialized responses, such as NextResponse.json, NextResponse.redirect, NextResponse.rewrite, and NextResponse.next.
| Static Method | Default Status / Header | Description |
|---|---|---|
NextResponse.json | Standard JSON init | Instantiates a response wrapping a JSON body |
NextResponse.redirect | Status 307 (validates 301, 302, 303, 307, 308) | Sets the Location header to the validated target URL |
NextResponse.rewrite | Header x-middleware-rewrite | Sets the rewrite destination and handles middleware request overrides |
NextResponse.next | Header x-middleware-next: '1' | Instructs the router to continue to the next middleware or route |
export class NextResponse<Body = unknown> extends Response {
[INTERNALS]: {
cookies: ResponseCookies
url?: NextURL
body?: Body
}
constructor(body?: BodyInit | null, init: ResponseInit = {}) {
super(body, init)
const headers = this.headers
const cookies = new ResponseCookies(headers)
const cookiesProxy = new Proxy(cookies, {
get(target, prop, receiver) {
switch (prop) {
case 'delete':
case 'set': {
return (...args: [string, string]) => {
const result = Reflect.apply(target[prop], target, args)
const newHeaders = new Headers(headers)
if (result instanceof ResponseCookies) {
headers.set(
'x-middleware-set-cookie',
result
.getAll()
.map((cookie) => stringifyCookie(cookie))
.join(',')
)
}
handleMiddlewareField(init, newHeaders)
return result
}
}
default:
return ReflectAdapter.get(target, prop, receiver)
}
},
})
this[INTERNALS] = {
cookies: cookiesProxy,
url: init.url
? new NextURL(init.url, {
headers: toNodeOutgoingHttpHeaders(headers),
nextConfig: init.nextConfig,
})
: undefined,
}
}
[Symbol.for('edge-runtime.inspect.custom')]() {
return {
cookies: this.cookies,
url: this.url,
// rest of props come from Response
body: this.body,
bodyUsed: this.bodyUsed,
headers: Object.fromEntries(this.headers),
ok: this.ok,
redirected: this.redirected,
status: this.status,
statusText: this.statusText,
type: this.type,
}
}
public get cookies() {
return this[INTERNALS].cookies
}
static json<JsonBody>(
body: JsonBody,
init?: ResponseInit
): NextResponse<JsonBody> {
const response: Response = Response.json(body, init)
return new NextResponse(response.body, response)
}Sources: packages/next/src/server/web/spec-extension/response.ts:36-153
Warning
Passing an invalid HTTP status code to NextResponse.redirect that is absent from the REDIRECTS set (301, 302, 303, 307, 308) throws a synchronous RangeError.
Sources: packages/next/src/server/web/spec-extension/response.ts:10-10, packages/next/src/server/web/spec-extension/response.ts:118-123
FetchEvent and WaitUntil Lifecycle
The adapter manages background task continuation using NextFetchEvent and internal waitUntil tracking. The internal_runWithWaitUntil function intercepts asynchronous promises passed to the runtime, incrementing a global waitUntilCounter and provisioning a shared waitUntilPromise that only resolves when all concurrent background operations complete.
export function internal_getCurrentFunctionWaitUntil() {
return state.waitUntilPromise
}
export function internal_runWithWaitUntil<T>(fn: () => T): T {
const result = fn()
if (
result &&
typeof result === 'object' &&
'then' in result &&
'finally' in result &&
typeof result.then === 'function' &&
typeof result.finally === 'function'
) {
if (!state.waitUntilCounter) {
// Create the promise for the next batch of waitUntil calls.
state.waitUntilPromise = new Promise<void>((resolve) => {
state.waitUntilResolve = resolve
})
}
state.waitUntilCounter++
return result.finally(() => {
resolveOnePromise()
})
}
return resultSources: packages/next/src/server/web/internal-edge-wait-until.ts:32-58
Note
Regardless of the number of concurrent requests executing in the edge sandbox, resolveOnePromise ensures the primary waitUntilPromise remains active until waitUntilCounter drops exactly to zero.
Sources: packages/next/src/server/web/internal-edge-wait-until.ts:21-30
During adapter execution, requests identified as middleware (/middleware, /src/middleware, /proxy, /src/proxy) are wrapped with execution telemetry via MiddlewareSpan.execute. The adapter sets up a CloseController to schedule resource cleanup via setTimeout upon completion, initializes request and work stores using createRequestStoreForAPI and createWorkStore, and executes the handler within workAsyncStorage and workUnitAsyncStorage contexts.
response = await propagator(request, () => {
// we only care to make async storage available for middleware
const isMiddleware =
params.page === '/middleware' ||
params.page === '/src/middleware' ||
params.page === '/proxy' ||
params.page === '/src/proxy'
if (isMiddleware) {
// if we're in an edge function, we only get a subset of `nextConfig` (no `experimental`),
// so we have to inject it via DefinePlugin.
// in `next start` this will be passed normally (see `NextNodeServer.runMiddleware`).
const waitUntil = event.waitUntil.bind(event)
const closeController = new CloseController()
return getTracer().trace(
MiddlewareSpan.execute,
{
spanName: `middleware ${request.method}`,
attributes: {
'http.target': request.nextUrl.pathname,
'http.method': request.method,
},
},
async () => {
try {
const onUpdateCookies = (cookies: Array<string>) => {
cookiesFromResponse = cookies
}
const previewProps = getEdgePreviewProps()
const page = '/' // Fake Work
const fallbackRouteParams = null
const implicitTags = await getImplicitTags(
page,
request.nextUrl.pathname,
fallbackRouteParams
)
const requestStore = createRequestStoreForAPI(
request,
request.nextUrl,
implicitTags,
onUpdateCookies,
previewProps,
// Edge route handlers can't use `"use cache"`, so there's no HMR
// refresh hash to thread through here.
undefined
)
const workStore = createWorkStore({
page,
renderOpts: {
cacheLifeProfiles: proxyCacheLifeProfiles,
// Proxy doesn't do static generation, so this value does not
// apply here. 0 is a sentinel: if something ever reads it,
// it'll surface loudly instead of silently using a misleading
// default.
staticPageGenerationTimeout: 0,
cacheComponents: false,
// Proxy doesn't run instant validation; the level value is
// irrelevant here.
// TODO: remove validationLevel and other global config from renderOpts
validationLevel: 'warning',
experimental: {
isRoutePPREnabled: false,
authInterrupts:
!!params.request.nextConfig?.experimental?.authInterrupts,
// Proxy doesn't fill Cache Components entries, so this value
// is never read. 0 is a sentinel: if something ever reads it,
// the cache fill will time out immediately and surface the
// bug.
useCacheTimeout: 0,
durableUseCacheEntries: false,
},
waitUntil,
onClose: closeController.onClose.bind(closeController),
onAfterTaskError: undefined,
},Sources: packages/next/src/server/web/adapter.ts:270-377
Route Module Edge Wrappers
Edge Route Module Wrapper Class
The EdgeRouteModuleWrapper class bridges edge request handlers and App Route modules. Constructed via its static wrap method, it accepts an AppRouteRouteModule instance along with configuration options like cacheHandlers and incrementalCacheHandler. The private constructor evaluates whether the route pathname is dynamic using isDynamicRoute.
export class EdgeRouteModuleWrapper {
private readonly pageIsDynamic: boolean
/**
* The constructor is wrapped with private to ensure that it can only be
* constructed by the static wrap method.
*
* @param routeModule the route module to wrap
*/
private constructor(
private readonly routeModule: AppRouteRouteModule,
private readonly cacheHandlers: Record<string, CacheHandler>
) {
this.pageIsDynamic = isDynamicRoute(routeModule.definition.pathname)
}Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:35-49
| Parameter / Property | Type | Description |
|---|---|---|
routeModule | AppRouteRouteModule | The target App Route module being executed |
cacheHandlers | Record<string, CacheHandler> | Custom cache handlers registered for the edge runtime |
incrementalCacheHandler | typeof IncrementalCacheHandler | Optional incremental cache class reference |
pageIsDynamic | boolean | Indicates if the route definition pathname contains dynamic segments |
export interface WrapOptions {
page: string
cacheHandlers?: Record<string, CacheHandler>
incrementalCacheHandler?: typeof IncrementalCacheHandler
}
/**
* EdgeRouteModuleWrapper is a wrapper around a route module.
*
* Note that this class should only be used in the edge runtime.
*/
export class EdgeRouteModuleWrapper {
private readonly pageIsDynamic: boolean
/**
* The constructor is wrapped with private to ensure that it can only be
* constructed by the static wrap method.
*
* @param routeModule the route module to wrap
*/
private constructor(
private readonly routeModule: AppRouteRouteModule,
private readonly cacheHandlers: Record<string, CacheHandler>
) {
this.pageIsDynamic = isDynamicRoute(routeModule.definition.pathname)
}Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:24-49
Request Execution and Context Construction
When the wrapped handler executes, it initializes server utilities, loads custom cache handlers based on nextConfig.cacheMaxMemorySize, and maps dynamic route parameters using normalizeDynamicRouteParams. It constructs an AppRouteRouteHandlerContext containing route parameters, preview properties, and render options.
private async handler(
request: NextRequestHint,
evt: NextFetchEvent
): Promise<Response> {
const utils = getServerUtils({
pageIsDynamic: this.pageIsDynamic,
page: this.routeModule.definition.pathname,
basePath: request.nextUrl.basePath,
// We don't need the `handleRewrite` util, so can just pass an empty object
rewrites: {},
// only used for rewrites, so setting an arbitrary default value here
caseSensitive: false,
})
const { nextConfig } = this.routeModule.getNextConfigEdge(
new WebNextRequest(request)
)
initializeCacheHandlers(nextConfig.cacheMaxMemorySize)
for (const [kind, cacheHandler] of Object.entries(this.cacheHandlers)) {
setCacheHandler(kind, cacheHandler)
}
const { params } = utils.normalizeDynamicRouteParams(
searchParamsToUrlQuery(request.nextUrl.searchParams),
false
)
const waitUntil = evt.waitUntil.bind(evt)
const closeController = new CloseController()
const previewProps = getEdgePreviewProps()
// Create the context for the handler. This contains the params from the
// match (if any).
const context: AppRouteRouteHandlerContext = {
params,
previewProps,
renderOpts: {
waitUntil,
onClose: closeController.onClose.bind(closeController),
onAfterTaskError: undefined,
cacheComponents: !!process.env.__NEXT_CACHE_COMPONENTS,
// Edge runtime doesn't run instant validation; the level value is
// irrelevant here.
// TODO: Remove validationLevel and other global config from renderOpts
validationLevel: 'warning',
experimental: {
authInterrupts: !!process.env.__NEXT_EXPERIMENTAL_AUTH_INTERRUPTS,
// Edge runtime doesn't support Cache Components, so this value is
// never read. 0 is a sentinel: if something ever reads it, the cache
// fill will time out immediately and surface the bug.
useCacheTimeout: 0,
durableUseCacheEntries: false,
},
cacheLifeProfiles: nextConfig.cacheLife,
// Edge runtime doesn't do static generation, so this value does not
// apply here. 0 is a sentinel: if something ever reads it, it'll
// surface loudly instead of silently using a misleading default.
staticPageGenerationTimeout: 0,
},
sharedContext: {
buildId: '', // TODO: Populate this properly.
deploymentId: '', // TODO: Populate this properly.
},
}Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:83-147
Caution
In the edge runtime, staticPageGenerationTimeout and useCacheTimeout are set to sentinel values of 0. Because edge environments do not support static generation or cache components, any accidental read forces an immediate timeout to expose the invalid configuration loudly.
Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:129-142
Background Task Handling and Response Tracking
After invoking this.routeModule.handle(request, context), the wrapper gathers pending background work by combining internal_getCurrentFunctionWaitUntil() and context.renderOpts.pendingWaitUntil into evt.waitUntil(). If the response lacks a body, a CloseController dispatches closure via setTimeout with a delay of zero; otherwise, trackStreamConsumed wraps the response body to trigger cleanup once consumption finishes.
// Get the response from the handler.
let res = await this.routeModule.handle(request, context)
const waitUntilPromises = [internal_getCurrentFunctionWaitUntil()]
if (context.renderOpts.pendingWaitUntil) {
waitUntilPromises.push(context.renderOpts.pendingWaitUntil)
}
evt.waitUntil(Promise.all(waitUntilPromises))
if (!res.body) {
// we can delay running it until a bit later --
// if it's needed, we'll have a `waitUntil` lock anyway.
setTimeout(() => closeController.dispatchClose(), 0)
} else {
// NOTE: if this is a streaming response, onClose may be called later,
// so we can't rely on `closeController.listeners` -- it might be 0 at this point.
const trackedBody = trackStreamConsumed(res.body, () =>
closeController.dispatchClose()
)
res = new Response(trackedBody, {
status: res.status,
statusText: res.statusText,
headers: res.headers,
})
}
return res
}Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:149-176
Note
For streaming responses, onClose cannot rely on pre-existing static listener counts because cleanup may be deferred until the stream body is fully read or consumed.
Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:163-167
Edge SSR and Middleware Templates
Pages SSR and App SSR Entrypoint Templates
Next.js generates dedicated edge entrypoint templates for Server-Side Rendering under both the Pages and App routers. The Pages SSR template (edge-ssr.ts) imports global polyfills, the adapter utility, and userland modules including Document, VAR_MODULE_APP, and VAR_USERLAND. It instantiates a Pages route module with pageRouteModuleOptions, configures cache handlers using nextConfig.cacheMaxMemorySize, and maps responses through renderResultToResponse.
import '../../server/web/globals'
import {
adapter,
type EdgeHandler,
type NextRequestHint,
} from '../../server/web/adapter'
import { IncrementalCache } from '../../server/lib/incremental-cache'
import * as cacheHandlers from '../../server/use-cache/handlers'
import Document from 'VAR_MODULE_DOCUMENT'
import * as appMod from 'VAR_MODULE_APP'
import * as userlandPage from 'VAR_USERLAND'
import * as userlandErrorPage from 'VAR_MODULE_GLOBAL_ERROR'
declare const userland500Page: any
declare const incrementalCacheHandler: any
// OPTIONAL_IMPORT:* as userland500Page
// OPTIONAL_IMPORT:incrementalCacheHandler
import RouteModule, {
type PagesRouteHandlerContext,
} from '../../server/route-modules/pages/module'
import { WebNextRequest, WebNextResponse } from '../../server/base-http/web'
import type { NextFetchEvent } from '../../server/web/spec-extension/fetch-event'
import type RenderResult from '../../server/render-result'
import type { RenderResultMetadata } from '../../server/render-result'
import {
getTracer,
SpanKind,
SpanStatusCode,
type Span,
} from '../../server/lib/trace/tracer'
import { BaseServerSpan } from '../../server/lib/trace/constants'
import { HTML_CONTENT_TYPE_HEADER } from '../../lib/constants'
import type { RequestMeta } from '../../server/request-meta'
import { toNodeOutgoingHttpHeaders } from '../../server/web/utils'
// injected by the loader afterwards.
declare const pageRouteModuleOptions: any
declare const errorRouteModuleOptions: any
declare const user500RouteModuleOptions: any
// INJECT:pageRouteModuleOptions
// INJECT:errorRouteModuleOptions
// INJECT:user500RouteModuleOptions
// INJECT_RAW:cacheHandlerImports
const pageMod = {
...userlandPage,
routeModule: new RouteModule({
...pageRouteModuleOptions,
components: {
App: appMod.default,
Document,
},
userland: userlandPage,
distDir: process.env.__NEXT_RELATIVE_DIST_DIR || '',
relativeProjectDir: process.env.__NEXT_RELATIVE_PROJECT_DIR || '',
}),
}Sources: packages/next/src/build/templates/edge-ssr.ts:1-60
cacheHandlers.initializeCacheHandlers(nextConfig.cacheMaxMemorySize)
// INJECT_RAW:cacheHandlerRegistration
const renderContext: PagesRouteHandlerContext = {
page: srcPage,
query,
params,
sharedContext: {
buildId,
deploymentId,
clientAssetToken,
customServer: undefined,
},
renderContext: {
isFallback: false,
isDraftMode: false,
developmentNotFoundSourcePage: undefined,
},
renderOpts: {
params,
page: srcPage,
supportsDynamicResponse: true,
Component: pageMod.Component,
ComponentMod: pageMod,
pageConfig: pageMod.pageConfig,
routeModule: pageMod.routeModule,
previewProps,
basePath: nextConfig.basePath,
assetPrefix: nextConfig.assetPrefix,
images: nextConfig.images,
optimizeCss: nextConfig.experimental.optimizeCss,
nextConfigOutput: nextConfig.output,
nextScriptWorkers: nextConfig.experimental.nextScriptWorkers,
disableOptimizedLoading: nextConfig.experimental.disableOptimizedLoading,
domainLocales: nextConfig.i18n?.domains,
distDir: '',
crossOrigin: nextConfig.crossOrigin ? nextConfig.crossOrigin : undefined,
largePageDataBytes: nextConfig.experimental.largePageDataBytes,
isExperimentalCompile: nextConfig.experimental.isExperimentalCompile,
// `htmlLimitedBots` is passed to server as serialized config in string format
experimental: {
clientTraceMetadata: nextConfig.experimental.clientTraceMetadata,
},
buildManifest,
subresourceIntegrityManifest,
reactLoadableManifest,
dynamicCssManifest,
},
}Sources: packages/next/src/build/templates/edge-ssr.ts:129-182
Similarly, the App SSR template (edge-ssr-app.ts) populates the React Server Component (RSC) manifest singletons by reading self.__RSC_MANIFEST and self.__RSC_SERVER_MANIFEST. It wraps page execution with AppPageRouteModule, detects server actions via getIsPossibleServerAction, and sets up incremental caching and stream tracking controllers.
import '../../server/web/globals'
import {
adapter,
type EdgeHandler,
type NextRequestHint,
} from '../../server/web/adapter'
import { IncrementalCache } from '../../server/lib/incremental-cache'
import * as pageMod from 'VAR_USERLAND'
import { setManifestsSingleton } from '../../server/app-render/manifests-singleton'
import * as cacheHandlers from '../../server/use-cache/handlers'
import { BaseServerSpan } from '../../server/lib/trace/constants'
import {
getTracer,
SpanKind,
SpanStatusCode,
type Span,
} from '../../server/lib/trace/tracer'
import { WebNextRequest, WebNextResponse } from '../../server/base-http/web'
import type { NextFetchEvent } from '../../server/web/spec-extension/fetch-event'
import type {
AppPageRouteHandlerContext,
AppPageRouteModule,
DevRenderContext,
RouteMatch,
} from '../../server/route-modules/app-page/module.compiled'
import type { AppPageRenderResultMetadata } from '../../server/render-result'
import type RenderResult from '../../server/render-result'
import { getIsPossibleServerAction } from '../../server/lib/server-action-request-meta'
import { getBotType } from '../../shared/lib/router/utils/is-bot'
import { shouldServeStreamingMetadata } from '../../server/lib/streaming-metadata'
import { interopDefault } from '../../lib/interop-default'
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
import { checkIsOnDemandRevalidate } from '../../server/api-utils'
import { CloseController } from '../../server/web/web-on-close'
import { parseMaxPostponedStateSize } from '../../shared/lib/size-limit'
import { toNodeOutgoingHttpHeaders } from '../../server/web/utils'
import type { RequestMeta } from '../../server/request-meta'
import { createDevRenderContext } from '../../server/route-modules/app-page/dev-render-context'
import {
parseRequestHeaders,
type ParsedRequestHeaders,
} from '../../server/route-modules/app-page/parse-request-headers'
declare const incrementalCacheHandler: any
// OPTIONAL_IMPORT:incrementalCacheHandler
// INJECT_RAW:cacheHandlerImports
const maybeJSONParse = (str?: string) => (str ? JSON.parse(str) : undefined)
const rscManifest = self.__RSC_MANIFEST?.['VAR_PAGE']
const rscServerManifest = maybeJSONParse(self.__RSC_SERVER_MANIFEST)
if (rscManifest && rscServerManifest) {
setManifestsSingleton({
page: 'VAR_PAGE',
clientReferenceManifest: rscManifest,
serverActionsManifest: rscServerManifest,
})
}
export const ComponentMod = pageMod
Sources: packages/next/src/build/templates/edge-ssr-app.ts:1-64
// Initialize the cache handlers interface.
cacheHandlers.initializeCacheHandlers(nextConfig.cacheMaxMemorySize)
// INJECT_RAW:cacheHandlerRegistration
const isPossibleServerAction = getIsPossibleServerAction(req)
const userAgent = req.headers.get('User-Agent') || ''
const botType = getBotType(userAgent)
const { isOnDemandRevalidate } = checkIsOnDemandRevalidate(
req.headers,
previewProps
)
const closeController = new CloseController()
const routeMatch: RouteMatch = { resolvedPathname }
const dev: DevRenderContext | undefined = createDevRenderContext(baseReq)
const renderContextBase: Omit<
AppPageRouteHandlerContext,
'parsedRequestHeaders'
> = {
page: normalizedSrcPage,
routeMatch,
query,
params,
dev,
sharedContext: {
buildId,
deploymentId,
clientAssetToken,
},Sources: packages/next/src/build/templates/edge-ssr-app.ts:106-136
Warning
In the edge SSR application template, serverComponentsHmrCancellation is explicitly set to false. Because the edge runtime lacks a Node response-close signal, HMR cancellation is treated as a no-op during development.
Sources: packages/next/src/build/templates/edge-ssr-app.ts:201-204
Middleware and Edge API Templates
The middleware template (middleware.ts) and Pages Edge API template (pages-edge-api.ts) handle lightweight edge routing and API handlers. The middleware template resolves userland code as either mod.proxy, mod.middleware, or mod.default. It verifies that the export is a function, throwing a ProxyMissingExportError if validation fails.
const mod = { ..._mod }
const page: string = 'VAR_DEFINITION_PAGE'
const isProxy = page === '/proxy' || page === '/src/proxy'
const handlerUserland = (isProxy ? mod.proxy : mod.middleware) || mod.default
class ProxyMissingExportError extends Error {
constructor(message: string) {
super(message)
// Stack isn't useful here, remove it considering it spams logs during development.
this.stack = ''
}
}
// TODO: This spams logs during development. Find a better way to handle this.
// Removing this will spam "fn is not a function" logs which is worse.
if (typeof handlerUserland !== 'function') {
throw new ProxyMissingExportError(
`The ${isProxy ? 'Proxy' : 'Middleware'} file "${page}" must export a function named \`${isProxy ? 'proxy' : 'middleware'}\` or a default function.`
)
}Sources: packages/next/src/build/templates/middleware.ts:17-37
| Export Target | Condition / Check | Error or Fallback Behavior |
|---|---|---|
mod.proxy | `page === '/proxy' | |
mod.middleware | Standard middleware path | Required to be a valid function export |
mod.default | Fallback export option | Throws ProxyMissingExportError if type is not a function |
const mod = { ..._mod }
const page: string = 'VAR_DEFINITION_PAGE'
const isProxy = page === '/proxy' || page === '/src/proxy'
const handlerUserland = (isProxy ? mod.proxy : mod.middleware) || mod.default
class ProxyMissingExportError extends Error {
constructor(message: string) {
super(message)
// Stack isn't useful here, remove it considering it spams logs during development.
this.stack = ''
}
}
// TODO: This spams logs during development. Find a better way to handle this.
// Removing this will spam "fn is not a function" logs which is worse.
if (typeof handlerUserland !== 'function') {
throw new ProxyMissingExportError(
`The ${isProxy ? 'Proxy' : 'Middleware'} file "${page}" must export a function named \`${isProxy ? 'proxy' : 'middleware'}\` or a default function.`
)
}Sources: packages/next/src/build/templates/middleware.ts:17-37
The errorHandledHandler wrapper intercepts runtime failures in middleware or proxy code. During non-production builds, if an error is identified as a router error via isNextRouterError, its message is modified to state that the Next.js navigation API is forbidden within middleware or proxy scopes before reporting the error through edgeInstrumentationOnRequestError.
function errorHandledHandler(fn: AdapterOptions['handler']) {
return async (...args: Parameters<AdapterOptions['handler']>) => {
try {
return await fn(...args)
} catch (err) {
// In development, error the navigation API usage in runtime,
// since it's not allowed to be used in proxy as it's outside of react component tree.
if (process.env.NODE_ENV !== 'production') {
if (isNextRouterError(err)) {
err.message = `Next.js navigation API is not allowed to be used in ${isProxy ? 'Proxy' : 'Middleware'}.`
throw err
}
}
const req = args[0]
const url = new URL(req.url)
const resource = url.pathname + url.search
await edgeInstrumentationOnRequestError(
err,
{
path: resource,
method: req.method,
headers: Object.fromEntries(req.headers.entries()),
},
{
routerKind: 'Pages Router',
routePath: '/proxy',
routeType: 'proxy',
revalidateReason: undefined,
}
)
throw err
}
}
}Sources: packages/next/src/build/templates/middleware.ts:41-75
Note
ProxyMissingExportError clears its error stack property during construction to eliminate log spamming caused by routine export validation checks during local development.
Sources: packages/next/src/build/templates/middleware.ts:23-29
Related
On This Page
OverviewEdge Compilation and Manifest PackagingWebpack Loaders for Edge EntrypointsMiddlewarePlugin Manifest GenerationGlobal Environment and Runtime PolyfillsEnvironment Bootstrapping and Global EnhancementRuntime Context Extension and Node API StubsDevelopment Sandbox Execution and IsolationModule Context Creation and Global InjectionRuntime Context Evaluation and Runner ExecutionError Tagging and Unhandled Event HandlingEdge Web Adapter ArchitectureRequest and Response ExtensionsFetchEvent and WaitUntil LifecycleRoute Module Edge WrappersEdge Route Module Wrapper ClassRequest Execution and Context ConstructionBackground Task Handling and Response TrackingEdge SSR and Middleware TemplatesPages SSR and App SSR Entrypoint TemplatesMiddleware and Edge API TemplatesRelated