How does the HTTP handler work in Next.js?

canary
02f8ba6feat: add TaskStorage derive macro and schema infrastructure (#88338)
This post might have stale content, as canary is 390 commits ahead.

How does the http handler works in nextjs?

Avatar of anthropic/claude-haiku-4.5
Claude Haiku 4.5
commented

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

  1. 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 Allow header 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 AsyncLocalStorage context 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 Allow header

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

How does the HTTP handler work in Next.js? — vercel/next.js