How does the HTTP handler work in Next.js?
How does the http handler works in nextjs?
Perfect! Now I have all the information I need. Let me create a comprehensive summary document:
##How the HTTP Handler Works in Next.js
The HTTP handler in Next.js is a sophisticated system that processes incoming HTTP requests and routes them to the appropriate handlers. Here's a detailed breakdown:
###High-Level Flow
- Request arrives → 2. Route matching → 3. Module loading → 4. Handler resolution → 5. Handler execution → 6. Response returned
###Architecture Components
####1. Route Definition & Matching
The route system uses RouteKind.APP_ROUTE for App Router routes and matches incoming requests against route definitions. Routes are defined in files like app/users/route.ts and are converted to route matchers that handle pattern matching (including dynamic segments like [id]).
From packages/next/src/server/route-definitions/app-route-route-definition.ts:
export interface AppRouteRouteDefinition
extends RouteDefinition<RouteKind.APP_ROUTE> {}
####2. Module Loading
When a request matches a route, the bundled route module is loaded using the RouteModuleLoader:
From packages/next/src/server/lib/module-loader/route-module-loader.ts:
export class RouteModuleLoader {
static async load<M extends RouteModule>(
id: string,
loader: ModuleLoader = new NodeModuleLoader()
): Promise<M> {
const module: AppLoaderModule<M> = await loader.load(id)
if ('routeModule' in module) {
return module.routeModule
}
throw new Error(`Module "${id}" does not export a routeModule.`)
}
}
####3. AppRouteRouteModule
The core handler class that processes requests. It's instantiated during compilation and wraps the user's route handler code.
From packages/next/src/server/route-modules/app-route/module.ts:
export class AppRouteRouteModule extends RouteModule<
AppRouteRouteDefinition,
AppRouteModule
> {
// ...
constructor({
userland,
definition,
distDir,
relativeProjectDir,
resolvedPagePath,
nextConfigOutput,
}: AppRouteRouteModuleOptions) {
// Auto-implement methods (HEAD, OPTIONS, etc.)
this.methods = autoImplementMethods(userland)
this.hasNonStaticMethods = hasNonStaticMethods(userland)
this.dynamic = this.userland.dynamic
}
}
###Request Handling Flow
####Step 1: Method Resolution (resolve())
When a request arrives, the HTTP method is extracted and validated:
private resolve(method: string): AppRouteHandlerFn {
// Ensure that the requested method is a valid method (to prevent RCE's)
if (!isHTTPMethod(method)) return () => new Response(null, { status: 400 })
// Return the handler
return this.methods[method]
}
Valid HTTP methods are defined in packages/next/src/server/web/http.ts:
export const HTTP_METHODS = [
'GET',
'HEAD',
'OPTIONS',
'POST',
'PUT',
'DELETE',
'PATCH',
] as const
####Step 2: Auto-Implementation
From packages/next/src/server/route-modules/app-route/helpers/auto-implement-methods.ts:
Automatically implements missing methods:
- HEAD: If not implemented but GET exists, HEAD automatically calls GET
- OPTIONS: Auto-generates the
Allowheader with all implemented methods
Example:
export function autoImplementMethods(
handlers: AppRouteHandlers
): Record<HTTP_METHOD, AppRouteHandlerFn> {
const methods: Record<HTTP_METHOD, AppRouteHandlerFn> = HTTP_METHODS.reduce(
(acc, method) => ({
...acc,
[method]: handlers[method] ?? handleMethodNotAllowedResponse,
}),
{} as Record<HTTP_METHOD, AppRouteHandlerFn>
)
// ... auto-implement HEAD and OPTIONS
}
####Step 3: Main Handler Execution (handle())
The handle() method is the main entry point:
public async handle(
req: NextRequest,
context: AppRouteRouteHandlerContext
): Promise<Response> {
// 1. Resolve the handler for the HTTP method
const handler = this.resolve(req.method)
// 2. Create context and stores
const staticGenerationContext: WorkStoreContext = {
page: this.definition.page,
renderOpts: context.renderOpts,
buildId: context.sharedContext.buildId,
previouslyRevalidatedTags: [],
}
const actionStore: ActionStore = {
isAppRoute: true,
isAction: getIsPossibleServerAction(req),
}
const requestStore = createRequestStoreForAPI(req, req.nextUrl, ...)
const workStore = createWorkStore(staticGenerationContext)
// 3. Run handler with async storage context
const response: unknown = await this.actionAsyncStorage.run(
actionStore,
() =>
this.workUnitAsyncStorage.run(requestStore, () =>
this.workAsyncStorage.run(workStore, async () => {
// Handle dynamic property
let request = req
switch (this.dynamic) {
case 'force-dynamic':
workStore.forceDynamic = true
break
case 'force-static':
workStore.forceStatic = true
request = new Proxy(req, forceStaticRequestHandlers)
break
case 'error':
workStore.dynamicShouldError = true
if (workStore.isStaticGeneration)
request = new Proxy(req, requireStaticRequestHandlers)
break
case 'auto':
case undefined:
request = proxyNextRequest(req, workStore)
break
}
// 4. Execute the handler through tracing
return tracer.trace(..., async () =>
this.do(
handler,
actionStore,
workStore,
requestStore,
implicitTags,
request,
context
)
)
})
)
)
// 5. Validate response and check for unsupported features
if (!(response instanceof Response)) {
return new Response(null, { status: 500 })
}
return response
}
####Step 4: Handler Execution (do())
The do() method handles the actual execution with special logic for:
Static Generation (Build Time):
- Runs handler in "prospective render" mode to detect if it's dynamic
- If using
cacheComponents, uses a two-phase approach:- Phase 1: Prospective render to fill caches and detect dynamic usage
- Phase 2: Final render with response body extraction within microtasks
Runtime Execution:
- Sets up
AsyncLocalStoragecontext for request data access - Patches fetch for caching and revalidation tracking
- Executes the user's handler function
private async do(
handler: AppRouteHandlerFn,
actionStore: ActionStore,
workStore: WorkStore,
requestStore: RequestStore,
implicitTags: ImplicitTags,
request: NextRequest,
context: AppRouteRouteHandlerContext
) {
// Patch global fetch
patchFetch({
workAsyncStorage: this.workAsyncStorage,
workUnitAsyncStorage: this.workUnitAsyncStorage,
})
const handlerContext: AppRouteHandlerFnContext = {
params: context.params
? createServerParamsForRoute(...)
: undefined,
}
let res: unknown
try {
if (isStaticGeneration) {
// Complex static generation logic...
res = await this.workUnitAsyncStorage.run(
prerenderStore,
handler,
request,
handlerContext
)
} else {
// Runtime execution
res = await workUnitAsyncStorage.run(
requestStore,
handler,
request,
handlerContext
)
}
} catch (err) {
// Handle redirects, HTTP access fallback errors, etc.
if (isRedirectError(err)) {
const url = getURLFromRedirectError(err)
const headers = new Headers({ Location: url })
appendMutableCookies(headers, requestStore.mutableCookies)
return new Response(null, {
status: actionStore.isAction
? RedirectStatusCode.SeeOther
: getRedirectStatusCodeFromError(err),
headers,
})
}
// ... other error handling
throw err
}
// Validate response
if (!(res instanceof Response)) {
throw new Error(
`No response is returned from route handler '${this.resolvedPagePath}'.
Ensure you return a \`Response\` or a \`NextResponse\` in all branches of your handler.`
)
}
// Merge cookies
const headers = new Headers(res.headers)
if (appendMutableCookies(headers, requestStore.mutableCookies)) {
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers,
})
}
return res
}
###Key Features
####1. AsyncLocalStorage Context
Three layers of async storage are maintained:
- actionAsyncStorage: Server action metadata
- workUnitAsyncStorage: Request-specific data and stores
- workAsyncStorage: Work/render context
This allows handlers to access helpers like cookies(), headers(), etc. without passing them as parameters.
####2. Dynamic Detection
Routes can be configured with a dynamic export:
'force-dynamic': Always dynamic, never statically generated'force-static': Always static, requests are proxied with empty data stubs'error': Error if any dynamic API is accessed'auto'(default): Track access to dynamic APIs, bail out if needed
####3. Automatic Method Handling
- Methods not explicitly implemented return 405 Method Not Allowed
- HEAD automatically delegates to GET if not implemented
- OPTIONS automatically generates the
Allowheader
####4. Cookie Management
Cookies can be mutated through the cookies() API and are merged back into the response headers before returning.
####5. Server Action Detection
The system detects if a request is from a server action (via getIsPossibleServerAction()) to apply appropriate handling.
####6. Static Generation Support
Route handlers can be statically generated at build time with proper cache invalidation and revalidation support.
###Example Flow
User Request: POST /api/users
↓
Route Matcher finds: app/api/users/route.ts
↓
RouteModuleLoader loads bundled module
↓
AppRouteRouteModule instantiated with userland handlers
↓
AppRouteRouteModule.handle() called
↓
resolve('POST') returns the POST handler
↓
AppRouteRouteModule.do() executes handler in async context
↓
User handler (POST function) runs:
- Can access cookies(), headers(), request.json(), etc.
- Returns Response object
↓
Response validated and cookies merged
↓
Response sent to client
This architecture provides a clean separation of concerns while enabling powerful features like static generation, cookie management, and dynamic tracking.
END OF POST