Architecture Overview
Server Runtime
Rendering Pipeline
Client Navigation
Caching and Export
Development Tools
Build and Configuration
Ecosystem Packages
Testing Infrastructure
How It Works
The following files were used as context for generating this wiki page:
Middleware execution in Next.js governs how incoming HTTP requests are intercepted, evaluated, and transformed before reaching core page handlers or router logic. By running custom code at the edge prior to request completion, it enables dynamic routing decisions, header manipulation, authentication checks, and URL rewrites or redirects based on runtime conditions.
Sources: packages/next/src/server/next-server.ts:1611-1617, packages/next/src/server/web/adapter.ts:377-380
Incoming request evaluation relies on matching paths against compiled regular expressions and verifying optional declarative conditions such as headers, cookies, query parameters, and host names. The routing subsystem processes these checks sequentially to determine whether middleware should execute for a given request.
Sources: packages/next/src/shared/lib/router/utils/middleware-route-matcher.ts:14-40, packages/next/src/shared/lib/router/utils/prepare-destination.ts:48-125
The route matching engine executes a deterministic call chain when testing an incoming request against configured route matchers.
unstable_doesMiddlewareMatch() initializes the validation context, parsing the target URL and constructing request abstractions with headers and cookies.
Sources: packages/next/src/experimental/testing/server/middleware-testing-utils.ts:19-40getMiddlewareRouteMatcher() iterates over the array of compiled proxy matchers, invoking RegExp.exec() against the request pathname.
Sources: packages/next/src/shared/lib/router/utils/middleware-route-matcher.ts:22-26matchHas() evaluates any associated has or missing conditions against the request headers, cookies, query, or host.
Sources: packages/next/src/shared/lib/router/utils/middleware-route-matcher.ts:28-33, packages/next/src/shared/lib/router/utils/prepare-destination.ts:48-125true; otherwise, iteration continues across remaining matchers or returns false.
Sources: packages/next/src/shared/lib/router/utils/middleware-route-matcher.ts:30-38Note
If a matcher configuration omits the matcher property entirely, unstable_doesMiddlewareMatch() immediately returns true without evaluating path regexes or conditional rules.
Sources: packages/next/src/experimental/testing/server/middleware-testing-utils.ts:32-34
The matchHas function inspects request properties based on explicit condition types defined in route configurations.
Warning
When evaluating cookie conditions against raw Node IncomingMessage objects, matchHas dynamically invokes getCookieParser() on request headers rather than relying on pre-parsed cookie collections.
Sources: packages/next/src/shared/lib/router/utils/prepare-destination.ts:66-72
Sources: packages/next/src/shared/lib/router/utils/middleware-route-matcher.ts:22-36, packages/next/src/shared/lib/router/utils/prepare-destination.ts:66-75, packages/next/src/shared/lib/router/utils/prepare-destination.ts:117-119
When an incoming HTTP request traverses the Next.js server routing pipeline, the system determines whether to execute middleware by matching request paths against compiled route criteria and inspecting internal routing metadata. The process evaluates explicit path matchers, handles data route normalization, and orchestrates target resolution before invoking the server request handler. Sources: packages/next-routing/src/resolve-routes.ts:492-555, packages/next/src/server/lib/router-utils/resolve-routes.ts:423-490
The decision to trigger middleware for a given request flows through a series of specific checks:
shouldInvokeMiddlewareForRequest() or resolveRoutes() inspects whether middlewareMatchers are defined; if undefined, legacy behavior defaults to returning true.
Sources: packages/next-routing/src/resolve-routes.ts:492-534middlewareMatchers is an empty array, it immediately short-circuits and returns false.
Sources: packages/next-routing/src/resolve-routes.ts:535-537url.pathname is tested against each matcher's compiled regular expression and conditional has or missing rules.
Sources: packages/next-routing/src/resolve-routes.ts:498-527decodeURIComponent(). If decoding throws an error, it returns false.
Sources: packages/next-routing/src/resolve-routes.ts:543-549false.
Sources: packages/next-routing/src/resolve-routes.ts:550-554Warning
Pathname decoding errors during middleware invocation checks are treated as non-fatal and immediately abort matching for that candidate, falling back to false rather than throwing a request-level exception.
Sources: packages/next-routing/src/resolve-routes.ts:546-548
During client transitions and server routing resolution, request metadata flags control how paths and data requirements are interpreted. The getMiddlewareData function inspects response headers to manage internal rewrites and redirection states.
Sources: packages/next/src/shared/lib/router/router.ts:183-199, packages/next/src/server/lib/router-server.ts:318-336
Note
When x-nextjs-data is present, if the normalized invokePath matches /404 while middleware matchers are configured, the server short-circuits execution, sets response status 404, and returns an empty JSON payload {} without invoking the full rendering pipeline.
Sources: packages/next/src/server/lib/router-server.ts:318-326
Sources: packages/next-routing/src/resolve-routes.ts:531-534, packages/next-routing/src/resolve-routes.ts:543-554, packages/next/src/server/lib/router-server.ts:333-343
Server-side orchestration prior to core request handling relies on manifest loading mechanisms and runtime inspection to locate, verify, and execute middleware before ordinary route handlers run. The NodeManifestLoader class uses the build distribution directory and the server directory constant to load compiled JSON manifests from disk. Concurrently, NextServer orchestrates the middleware execution pipeline through runMiddleware, validating requests, handling on-demand revalidation bypasses, normalizing absolute URLs, and evaluating edge versus Node.js middleware runtimes.
Sources: packages/next/src/server/route-matcher-providers/helpers/manifest-loaders/node-manifest-loader.ts:5-20, packages/next/src/server/next-server.ts:1617-1760
When the server initializes and resolves routing metadata or evaluates route matchers, manifest loaders retrieve structural configuration files from the build output directory. For middleware and routing entries, the call sequence proceeds through manifest lookup and verification functions before request handling commences.
The execution call chain for loading manifests and dispatching middleware proceeds through the following sequence:
NodeManifestLoader.load() — Joins the distribution directory (distDir), the server directory constant (SERVER_DIRECTORY), and the requested manifest name to locate the file on disk.
Sources: packages/next/src/server/route-matcher-providers/helpers/manifest-loaders/node-manifest-loader.ts:16-20NodeManifestLoader.require() — Safely executes a Node.js require() on the constructed file path, returning null if the manifest file is absent or fails to load.
Sources: packages/next/src/server/route-matcher-providers/helpers/manifest-loaders/node-manifest-loader.ts:8-14runMiddleware() — Orchestrates runtime request evaluation in NextServer, verifying on-demand revalidation status, normalizing request protocols, and querying this.getMiddleware() and this.hasMiddleware().
Sources: packages/next/src/server/next-server.ts:1617-1675this.getEdgeFunctionInfo() or this.loadNodeMiddleware() — Resolves the execution target based on whether edge function metadata exists in the middleware manifest or falls back to Node.js middleware modules.
Sources: packages/next/src/server/next-server.ts:1678-1714adapterFn() or sandbox.run() — Executes the resolved middleware module via the web adapter or the isolated sandbox runtime, returning headers, cookies, and waitUntil promises.
Sources: packages/next/src/server/next-server.ts:1719-1759Warning
If process.env.NEXT_MINIMAL is set, calling runMiddleware() throws an immediate invariant error because minimal mode expects server orchestration to bypass standard middleware runner execution.
Sources: packages/next/src/server/next-server.ts:1624-1628
The orchestration layer relies on constant paths, environment configuration flags, and request metadata to determine how middleware is loaded and invoked.
Sources: packages/next/src/server/route-matcher-providers/helpers/manifest-loaders/node-manifest-loader.ts:1-18, packages/next/src/server/next-server.ts:1624-1628, packages/next/src/server/next-server.ts:1801-1805, packages/next/src/server/next-server.ts:1914-1916
Sources: packages/next/src/server/route-matcher-providers/helpers/manifest-loaders/node-manifest-loader.ts:8-14, packages/next/src/server/next-server.ts:1630-1640, packages/next/src/server/next-server.ts:1712-1721
The web adapter layer converts standard Node HTTP request structures and route module handlers into Fetch API-compatible abstractions (NextRequest, NextResponse, NextFetchEvent), while parsing and validating the response objects returned by middleware execution.
Sources: packages/next/src/server/web/adapter.ts:1-10, packages/next/src/server/web/edge-route-module-wrapper.ts:71-82
The execution of a middleware request through the adapter pipeline flows through specific phases from wrapping to response parsing:
EdgeRouteModuleWrapper.wrap() initializes the module wrapper and binds the wrapper's private handler() method as an EdgeHandler.
Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:61-82adapter() receives request options, sets up the outer waitUntil promise context via getBuiltinRequestContext(), and instantiates NextFetchEvent.
Sources: packages/next/src/server/web/adapter.ts:241-250propagator(), which checks if params.page corresponds to middleware (/middleware, /src/middleware, /proxy, or /src/proxy). If true, it initiates a tracer span (MiddlewareSpan.execute), creates request and work stores via createRequestStoreForAPI() and createWorkStore(), and executes workAsyncStorage.run() wrapping params.handler.
Sources: packages/next/src/server/web/adapter.ts:254-346EdgeRouteModuleWrapper.handler() normalizes dynamic route parameters via utils.normalizeDynamicRouteParams(), instantiates a CloseController, calls this.routeModule.handle(request, context), and attaches stream tracking via trackStreamConsumed().
Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:84-176responseToMiddlewareResult() converts the returned middleware Response object into a mutable MiddlewareResult, processing header overrides (x-middleware-override-headers), rewrites (x-middleware-rewrite), redirects (location), and refresh signals (x-middleware-refresh).
Sources: packages/next-routing/src/middleware.ts:13-204Caution
If the value returned from params.handler is truthy but fails to satisfy response instanceof Response, the adapter immediately throws a TypeError('Expected an instance of Response to be returned'), halting request propagation.
Sources: packages/next/src/server/web/adapter.ts:362-365
The response handling logic inspects specific custom headers to determine how request headers and downstream routing destinations are mutated.
Sources: packages/next/src/server/web/adapter.ts:347-365, packages/next/src/server/web/edge-route-module-wrapper.ts:159-174, packages/next-routing/src/middleware.ts:141-189
Executing edge middleware within isolated runtime environments requires constructing dedicated module contexts, managing asynchronous execution scopes, and stripping restricted transport headers. The sandbox architecture coordinates runtime initialization, request body stream cloning, and error stack-frame mapping for development mode.
The sandbox entry point orchestrates runtime setup and handler invocation through a sequence of discrete operations.
getRuntimeContext(params) calls getModuleContext() to acquire the compiled edge runtime and evaluation helper, attaches incremental caching (__incrementalCache, __incrementalCacheShared), router server context symbols, HMR caches (__serverComponentsHmrCache), and client asset tokens (NEXT_CLIENT_ASSET_SUFFIX), then evaluates all provided module paths.
Sources: packages/next/src/server/web/sandbox/sandbox.ts:72-109run(params) wraps the execution flow using withTaggedErrors, invokes getRuntimeContext(params) to retrieve the runtime, and extracts the default export function from runtime.context._ENTRIES['middleware_' + params.name].
Sources: packages/next/src/server/web/sandbox/sandbox.ts:49-70, packages/next/src/server/web/sandbox/sandbox.ts:111-116['HEAD', 'GET'].includes(params.request.method). For methods carrying payloads, params.request.body?.cloneBodyStream() duplicates the stream.
Sources: packages/next/src/server/web/sandbox/sandbox.ts:118-120edgeSandboxNextRequestContext.run() and requestStore.run() establish the async local storage contexts before executing the edgeFunction with a normalized request object that wraps cloned streams via requestToBodyStream().
Sources: packages/next/src/server/web/sandbox/sandbox.ts:134-155FORBIDDEN_HEADERS are systematically deleted from the resulting response object, and the request body stream is finalized in a finally block.
Sources: packages/next/src/server/web/sandbox/sandbox.ts:23-27, packages/next/src/server/web/sandbox/sandbox.ts:152-162Sources: packages/next/src/server/web/sandbox/sandbox.ts:21-27, packages/next/src/server/web/sandbox/sandbox.ts:84-87, packages/next/src/server/web/sandbox/sandbox.ts:100-103
Sources: packages/next/src/server/web/sandbox/sandbox.ts:49-70, packages/next/src/server/web/sandbox/sandbox.ts:118-120, packages/next/src/server/web/sandbox/sandbox.ts:152-162
Warning
If an edge function execution fails to return a response object (resulting in an undefined result), run() immediately throws an explicit Error('Edge function did not return a response') rather than defaulting to an empty response.
Sources: packages/next/src/server/web/sandbox/sandbox.ts:158-158
The following example demonstrates how run initializes runtime context, handles request body stream cloning for non-GET/HEAD methods, and delegates execution through nested async storage contexts.
import { run } from './sandbox'
import type { NodejsRequestData, FetchEventResult } from '../types'
async function executeMiddlewareSandbox(
requestData: NodejsRequestData,
modulePaths: string[]
): Promise<FetchEventResult> {
const result = await run({
name: 'middleware',
paths: modulePaths,
request: requestData,
useCache: true,
edgeFunctionEntry: {
assets: [],
wasm: [],
env: {},
},
distDir: '.next',
clientAssetToken: 'secure-token-123',
onError: (err) => console.error('Sandbox error:', err),
onWarning: (warn) => console.warn('Sandbox warning:', warn),
})
return result
}Sources: packages/next/src/server/web/sandbox/sandbox.ts:29-43, packages/next/src/server/web/sandbox/sandbox.ts:111-163
The development bundler and hot reloader coordinate on-demand compilation, file watching, and source frame resolution for middleware and application routes. File watchers monitor project directories with an aggregate timeout of 5 milliseconds to detect modifications to configuration, environment files, or route entrypoints. When changes occur, the bundler aggregates file states, validates convention files, and maps source locations for error overlays.
File change detection and entrypoint evaluation execute through a structured asynchronous pipeline when Watchpack triggers an aggregation event:
wp.on('aggregated') — Fires when file modifications settle past the 5ms aggregate timeout.wp.getTimeInfoEntries() — Retrieves timestamps and metadata for all known files in the project workspace.absolutePathToPage() — Normalizes scanned file paths into internal route identifiers based on page extensions and directory structure.isMiddlewareFile() — Evaluates whether a modified file matches the middleware convention (middleware.ts or proxy.ts).getStaticInfoIncludingLayouts() — Inspects static configuration and extracts middleware matchers from the file payload.Sources: packages/next/src/server/lib/router-utils/setup-dev-bundler.ts:422-458, packages/next/src/server/lib/router-utils/setup-dev-bundler.ts:548-567
Warning
If both middleware.ts and proxy.ts are detected at the root convention level, the dev bundler immediately throws an error requiring the use of proxy.ts exclusively.
Sources: packages/next/src/server/lib/router-utils/setup-dev-bundler.ts:479-486
Source maps and original stack frames are resolved through middleware handlers that query client, server, and edge server webpack compilations. The resolution sequence inspects native node source maps or webpack bundle modules to map compiled line and column positions back to original source files.
Sources: packages/next/src/server/dev/middleware-webpack.ts:592-677, packages/next/src/server/dev/middleware-webpack.ts:697-744
Tip
When resolving original stack frames, sources containing node_modules, next/dist, or starting with node: are automatically marked as ignored unless explicitly requested otherwise.
Sources: packages/next/src/server/dev/middleware-webpack.ts:36-43, packages/next/src/server/dev/middleware-webpack.ts:237-243
Sources: packages/next/src/server/lib/router-utils/setup-dev-bundler.ts:402-403, packages/next/src/server/dev/middleware-webpack.ts:36-43, packages/next/src/server/dev/middleware-webpack.ts:485-515