Skip to main content
doc0blade47/next.jsdocumentation home
Sign in

Server Architecture

Last updated Oct 12, 202692% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

The Next.js server architecture provides the foundational runtime environment, HTTP request processing pipeline, and modular routing infrastructure that drives both development and production servers. It handles programmatic instantiation via wrapper classes, orchestrates process socket binding and listener bootstrapping, and implements abstract base server contracts specialized for Node.js execution. By normalizing low-level Node.js streams into Web standard request and response abstractions, the server architecture unifies handling across disparate runtimes while managing robust error detection, bubbling, and request metadata propagation throughout the rendering lifecycle.

Sources: packages/next/src/server/next.ts:179-190, packages/next/src/server/lib/router-server.ts:131-145, packages/next/src/server/base-server.ts:477-492, packages/next/src/server/next-server.ts:198-202, packages/next/src/server/base-http/node.ts:19-27, packages/next/src/server/request-meta.ts:51-110

Public Server Entrypoints

Wrapper Server and Lifecycle Configuration

The NextServer wrapper class implements NextWrapperServer to serve as the programmatic public interface for custom servers and commands like next start. It manages asynchronous configuration loading via loadConfig, maps runtime phases to PHASE_DEVELOPMENT_SERVER or PHASE_PRODUCTION_SERVER, and initializes either NextNodeServer or NextDevServer depending on options.dev.

export class NextServer implements NextWrapperServer {
  private serverPromise?: Promise<NextNodeServer>
  private server?: NextNodeServer
  private reqHandler?: NodeRequestHandler
  private reqHandlerPromise?: Promise<NodeRequestHandler>
  private preparedAssetPrefix?: string
 
  public options: NextServerOptions
 
  constructor(options: NextServerOptions) {
    this.options = options
  }

Sources: packages/next/src/server/next.ts:179-190

private async [SYMBOL_LOAD_CONFIG]() {
  const dir = path.resolve(
    /* turbopackIgnore: true */ this.options.dir || '.'
  )
 
  const config = await loadConfig(
    this.options.dev ? PHASE_DEVELOPMENT_SERVER : PHASE_PRODUCTION_SERVER,
    dir,
    {
      customConfig: this.options.conf,
      silent: true,
    }
  )
 

Sources: packages/next/src/server/next.ts:342-355

Warning

NextServer enforces strict checks on production output configurations during initialization. If conf.output === 'standalone' is set without __NEXT_PRIVATE_STANDALONE_CONFIG, a warning is logged, whereas conf.output === 'export' throws an immediate error instructing the user to use serve instead.

if (!this.options.dev) {
  if (conf.output === 'standalone') {
    if (!process.env.__NEXT_PRIVATE_STANDALONE_CONFIG) {
      log.warn(
        `"next start" does not work with "output: standalone" configuration. Use "node .next/standalone/server.js" instead.`
      )
    }
  } else if (conf.output === 'export') {
    throw new Error(
      `"next start" does not work with "output: export" configuration. Use "npx serve@latest out" instead.`
    )
  }
}

Sources: packages/next/src/server/next.ts:389-401

Deprecated Custom Server Methods

Custom server instances expose several legacy methods that are guarded with runtime warnings via warnDeprecatedCustomServerMethod. The mapping of deprecated methods to alternative guidance is maintained in DEPRECATED_CUSTOM_SERVER_METHOD_GUIDANCE.

Deprecated MethodGuidance Message
setAssetPrefixPlease configure assetPrefix in next.config.js instead.
logErrorPlease use application logging instead.
logErrorWithOriginalStackPlease use application logging instead.
revalidatePlease use documented application revalidation APIs instead.
renderPlease use app.getRequestHandler() with an adjusted parsed URL instead.
renderToHTMLPlease use app.getRequestHandler() with an adjusted parsed URL instead.
renderErrorPlease use app.getRequestHandler() with an adjusted parsed URL instead.
renderErrorToHTMLPlease use app.getRequestHandler() with an adjusted parsed URL instead.
render404Please use app.getRequestHandler() with an adjusted parsed URL instead.
const DEPRECATED_CUSTOM_SERVER_METHOD_GUIDANCE: Record<
  DeprecatedCustomServerMethod,
  string
> = {
  setAssetPrefix: 'Please configure `assetPrefix` in `next.config.js` instead.',
  logError: 'Please use application logging instead.',
  logErrorWithOriginalStack: 'Please use application logging instead.',
  revalidate: 'Please use documented application revalidation APIs instead.',
  render:
    'Please use `app.getRequestHandler()` with an adjusted parsed URL instead.',
  renderToHTML:
    'Please use `app.getRequestHandler()` with an adjusted parsed URL instead.',
  renderError:
    'Please use `app.getRequestHandler()` with an adjusted parsed URL instead.',
  renderErrorToHTML:
    'Please use `app.getRequestHandler()` with an adjusted parsed URL instead.',
  render404:
    'Please use `app.getRequestHandler()` with an adjusted parsed URL instead.',
}

Sources: packages/next/src/server/next.ts:84-102

Server Initialization and Bootstrapping

Baseline Runtime Environment and Server Startup

Before loading any application modules or establishing network sockets, the server establishes its baseline runtime environment via node-environment-baseline.ts. This initialization step ensures that AsyncLocalStorage from async_hooks is exposed on globalThis for React context propagation if the host environment does not provide it natively. Additionally, it defines a getter property on globalThis for WebSocket, lazily requiring next/dist/compiled/ws when accessed.

if (typeof (globalThis as any).AsyncLocalStorage !== 'function') {
  const { AsyncLocalStorage } =
    require('async_hooks') as typeof import('async_hooks')
  ;(globalThis as any).AsyncLocalStorage = AsyncLocalStorage
}
 
if (typeof (globalThis as any).WebSocket !== 'function') {
  Object.defineProperty(globalThis, 'WebSocket', {
    configurable: true,
    get() {
      return (
        require('next/dist/compiled/ws') as typeof import('next/dist/compiled/ws')
      ).WebSocket
    },
    set(value) {
      Object.defineProperty(globalThis, 'WebSocket', {
        configurable: true,
        writable: true,
        value,
      })
    },
  })

Sources: packages/next/src/server/node-environment-baseline.ts:5-26

The core bootstrapping function startServer in start-server.ts updates process.title to next-server (v${process.env.__NEXT_VERSION}) and instantiates the underlying HTTP listener. Depending on whether selfSignedCertificate options and production constraints are met, it invokes either https.createServer or http.createServer with a requestListener.

let { port } = serverOptions
 
process.title = `next-server (v${process.env.__NEXT_VERSION})`
let handlersReady = () => {}
let handlersError = () => {}
 
let handlersPromise: Promise<void> | undefined = new Promise<void>(
  (resolve, reject) => {
    handlersReady = resolve
    handlersError = reject
  }
)
let requestHandler: WorkerRequestHandler = async (
  req: IncomingMessage,
  res: ServerResponse
): Promise<void> => {
  if (handlersPromise) {
    await handlersPromise
    return requestHandler(req, res)
  }
  throw new Error('Invariant request handler was not setup')
}
let upgradeHandler: WorkerUpgradeHandler = async (
  req,
  socket,
  head
): Promise<void> => {
  if (handlersPromise) {
    await handlersPromise
    return upgradeHandler(req, socket, head)
  }
  throw new Error('Invariant upgrade handler was not setup')
}
 
let nextServer: NextServer | undefined
let devMemoryThresholdRestart = true
 
// setup server listener as fast as possible
if (selfSignedCertificate && !isDev) {
  throw new Error(
    'Using a self signed certificate is only supported with `next dev`.'
  )
}
 
async function requestListener(req: IncomingMessage, res: ServerResponse) {
  try {
    if (handlersPromise) {
      await handlersPromise
      handlersPromise = undefined
    }
    await requestHandler(req, res)
  } catch (err) {
    res.statusCode = 500
    res.end('Internal Server Error')
    Log.error(`Failed to handle request for ${req.url}`)
    console.error(err)
  } finally {
    const memoryRestartStats = getMemoryRestartStats(
      isDev,
      devMemoryThresholdRestart,
      v8.getHeapStatistics
    )
    if (memoryRestartStats) {
      Log.warn(
        `Server is approaching the used memory threshold, restarting...`
      )
      trace('server-restart-close-to-memory-threshold', undefined, {
        'memory.heapSizeLimit': String(memoryRestartStats.heap_size_limit),
        'memory.heapUsed': String(memoryRestartStats.used_heap_size),
      }).stop()
      flushAllTraces()
      process.exit(RESTART_EXIT_CODE)
    }
  }
}
 
const server = selfSignedCertificate
  ? https.createServer(
      {
        key: fs.readFileSync(selfSignedCertificate.key),

Sources: packages/next/src/server/lib/start-server.ts:204-288

Caution

Using a self-signed certificate via selfSignedCertificate is strictly restricted to development mode (isDev). Attempting to use a self-signed certificate when isDev is false causes startServer to throw an immediate error during listener setup.

if (selfSignedCertificate && !isDev) {
  throw new Error(
    'Using a self signed certificate is only supported with `next dev`.'
  )
}

Sources: packages/next/src/server/lib/start-server.ts:242-246

Socket Binding, Port Allocation, and Termination Signals

Once the server instance is configured with optional keepAliveTimeout and upgrade event handlers, it binds to the specified port and hostname. If allowRetry is enabled and an EADDRINUSE error occurs on error, the development server automatically increments the port by one and retries up to 10 times before logging a fatal startup failure and exiting the process with code 1.

if (keepAliveTimeout) {
  server.keepAliveTimeout = keepAliveTimeout
}
server.on('upgrade', async (req, socket, head) => {
  try {
    await upgradeHandler(req, socket, head)
  } catch (err) {
    socket.destroy()
    Log.error(`Failed to handle request for ${req.url}`)
    console.error(err)
  }
})
 
let portRetryCount = 0
const originalPort = port
 
server.on('error', (err: NodeJS.ErrnoException) => {
  if (
    allowRetry &&
    port &&
    isDev &&
    err.code === 'EADDRINUSE' &&
    portRetryCount < 10
  ) {
    port += 1
    portRetryCount += 1
    server.listen(port, hostname)
  } else {
    Log.error(`Failed to start server`)
    console.error(err)
    process.exit(1)
  }
})

Sources: packages/next/src/server/lib/start-server.ts:290-322

Upon successfully emitting the listening event, the server registers signal handlers for graceful process termination unless NEXT_MANUAL_SIG_HANDLE is set in the environment.

SignalExit Code Behavior
SIGINTExits process with code 130 (signal termination)
SIGTERMExits process with code 143 (signal termination)
Default / UnhandledExits process with code 128
    switch (signal) {
      case 'SIGINT':
        process.exit(130)
        break
      case 'SIGTERM':
        process.exit(143)
        break
      default:
        // Make sure all handled signals have explicit exit codes.
        // This is just a fallback to guard against unsound types.
        signal satisfies never
        process.exit(128)
    }
  })()
}
 
// Make sure commands gracefully respect termination signals (e.g. from Docker)
// Allow the graceful termination to be manually configurable
if (!process.env.NEXT_MANUAL_SIG_HANDLE) {
  process.on('SIGINT', cleanup)
  process.on('SIGTERM', cleanup)
}

Sources: packages/next/src/server/lib/start-server.ts:471-492

BaseServer Architecture and Core Contracts

Abstract Server Responsibilities and Constructor Initialization

BaseServer serves as the abstract coordinator for Next.js HTTP request handling, environment configuration, and asset normalization across different execution runtimes. The constructor initializes configuration values, sets up path resolvers, and instantiates request normalizers based on whether app directories, pages directories, or internationalization settings are active.

public constructor(options: ServerOptions) {
  const {
    dir = '.',
    quiet = false,
    conf,
    dev = false,
    minimalMode = false,
    hostname,
    port,
    experimentalTestProxy,
  } = options
 
  this.dev = dev
  this.experimentalTestProxy = experimentalTestProxy
  this.serverOptions = options
 
  this.dir = path.resolve(/* turbopackIgnore: true */ dir)
 
  this.quiet = quiet
  this.loadEnvConfig({ dev, forceReload: false })
 
  // TODO: should conf be normalized to prevent missing
  // values from causing issues as this can be user provided
  this.nextConfig = conf as NextConfigRuntime
  if (
    (dev || process.env.__NEXT_DEV_SERVER) &&
    this.nextConfig.experimental.requestInsights
  ) {
    process.env.__NEXT_REQUEST_INSIGHTS = 'true'
  }
 
  if (this.nextConfig.experimental.runtimeServerDeploymentId) {
    if (!process.env.NEXT_DEPLOYMENT_ID) {
      throw new Error(
        'process.env.NEXT_DEPLOYMENT_ID is missing but runtimeServerDeploymentId is enabled'
      )
    }
    this.deploymentId = process.env.NEXT_DEPLOYMENT_ID
  } else {
    let id = this.nextConfig.experimental.useSkewCookie
      ? ''
      : this.nextConfig.deploymentId || ''
 
    this.deploymentId = id
    process.env.NEXT_DEPLOYMENT_ID = id
  }
  ;(globalThis as any).NEXT_CLIENT_ASSET_SUFFIX =
    this.nextConfig.supportsImmutableAssets || !this.deploymentId
      ? ''
      : `?dpl=${this.deploymentId}`
 
  this.hostname = hostname
  if (this.hostname) {
    // we format the hostname so that it can be fetched
    this.fetchHostname = formatHostname(this.hostname)
  }
  this.port = port
  this.distDir = path.join(
    /* turbopackIgnore: true */ this.dir,
    this.nextConfig.distDir
  )
  this.pagesNormalizers = new PagesNormalizers(this.distDir)
  this.appNormalizers = new AppNormalizers(this.distDir)
  this.publicDir = this.getPublicDir()
  this.hasStaticDir = !minimalMode && this.getHasStaticDir()
 
  this.i18nProvider = this.nextConfig.i18n?.locales
    ? new I18NProvider(this.nextConfig.i18n)
    : undefined
 
  // Configure the locale normalizer, it's used for routes inside `pages/`.
  this.localeNormalizer = this.i18nProvider
    ? new LocaleRouteNormalizer(this.i18nProvider)
    : undefined
 
  const { assetPrefix, generateEtags } = this.nextConfig
 
  this.buildId = this.getBuildId()
  // this is a hack to avoid Webpack knowing this is equal to this.minimalMode
  // because we replace this.minimalMode to true in production bundles.

Sources: packages/next/src/server/base-server.ts:477-580

During initialization, the server resolves the root application directory (dir), configures deployment identifiers via process.env.NEXT_DEPLOYMENT_ID or runtimeServerDeploymentId, and sets up locale normalizers if internationalization is enabled. It configures path normalizers for Pages (PagesNormalizers) and App (AppNormalizers) routes within the distribution directory (distDir).

this.dir = path.resolve(/* turbopackIgnore: true */ dir)
 
this.quiet = quiet
this.loadEnvConfig({ dev, forceReload: false })
 
// TODO: should conf be normalized to prevent missing
// values from causing issues as this can be user provided
this.nextConfig = conf as NextConfigRuntime
if (
  (dev || process.env.__NEXT_DEV_SERVER) &&
  this.nextConfig.experimental.requestInsights
) {
  process.env.__NEXT_REQUEST_INSIGHTS = 'true'
}
 
if (this.nextConfig.experimental.runtimeServerDeploymentId) {
  if (!process.env.NEXT_DEPLOYMENT_ID) {
    throw new Error(
      'process.env.NEXT_DEPLOYMENT_ID is missing but runtimeServerDeploymentId is enabled'
    )
  }
  this.deploymentId = process.env.NEXT_DEPLOYMENT_ID
} else {
  let id = this.nextConfig.experimental.useSkewCookie
    ? ''
    : this.nextConfig.deploymentId || ''
 
  this.deploymentId = id
  process.env.NEXT_DEPLOYMENT_ID = id
}
;(globalThis as any).NEXT_CLIENT_ASSET_SUFFIX =
  this.nextConfig.supportsImmutableAssets || !this.deploymentId
    ? ''
    : `?dpl=${this.deploymentId}`
 
this.hostname = hostname
if (this.hostname) {
  // we format the hostname so that it can be fetched
  this.fetchHostname = formatHostname(this.hostname)
}
this.port = port
this.distDir = path.join(
  /* turbopackIgnore: true */ this.dir,
  this.nextConfig.distDir
)
this.pagesNormalizers = new PagesNormalizers(this.distDir)
this.appNormalizers = new AppNormalizers(this.distDir)
this.publicDir = this.getPublicDir()
this.hasStaticDir = !minimalMode && this.getHasStaticDir()

Sources: packages/next/src/server/base-server.ts:493-541

Caution

If runtimeServerDeploymentId is enabled in nextConfig.experimental, process.env.NEXT_DEPLOYMENT_ID must be defined beforehand. If the environment variable is missing during server startup, BaseServer throws an immediate error.

if (this.nextConfig.experimental.runtimeServerDeploymentId) {
  if (!process.env.NEXT_DEPLOYMENT_ID) {
    throw new Error(
      'process.env.NEXT_DEPLOYMENT_ID is missing but runtimeServerDeploymentId is enabled'
    )
  }
  this.deploymentId = process.env.NEXT_DEPLOYMENT_ID

Sources: packages/next/src/server/base-server.ts:508-514

Normalizers and Render Options Configuration

The constructor constructs a core set of request path normalizers and builds the internal renderOpts object passed down to rendering pipelines. The normalizers object routes incoming requests through specialized prefix checkers depending on whether the server operates in minimal mode or handles server components.

Normalizer PropertyClass Type / Condition
rscRSCPathnameNormalizer (Active if enabledDirectories.app and minimalMode are true)
segmentPrefetchRSCSegmentPrefixRSCPathnameNormalizer (Active if minimalMode is true)
dataNextDataPathnameNormalizer (Active if enabledDirectories.pages is true, initialized with buildId)
this.normalizers = {
  // We should normalize the pathname from the RSC prefix only in minimal
  // mode as otherwise that route is not exposed external to the server as
  // we instead only rely on the headers.
  rsc:
    this.enabledDirectories.app && this.minimalMode
      ? new RSCPathnameNormalizer()
      : undefined,
  segmentPrefetchRSC: this.minimalMode
    ? new SegmentPrefixRSCPathnameNormalizer()
    : undefined,
  data: this.enabledDirectories.pages
    ? new NextDataPathnameNormalizer(this.buildId)
    : undefined,
}

Sources: packages/next/src/server/base-server.ts:566-580

The server also compiles renderOpts using configuration options extracted from this.nextConfig, including base paths, asset prefixes, experimental feature flags like taint support, script workers, optimize CSS, and internationalized domain settings.

this.renderOpts = {
  dir: this.dir,
  supportsDynamicResponse: true,
  trailingSlash: this.nextConfig.trailingSlash,
  poweredByHeader: this.nextConfig.poweredByHeader,
  generateEtags,
  previewProps: this.getPreviewProps(),
  basePath: this.nextConfig.basePath,
  images: this.nextConfig.images,
  optimizeCss: this.nextConfig.experimental.optimizeCss,
  nextConfigOutput: this.nextConfig.output,
  nextScriptWorkers: this.nextConfig.experimental.nextScriptWorkers,
  disableOptimizedLoading:
    this.nextConfig.experimental.disableOptimizedLoading,
  domainLocales: this.nextConfig.i18n?.domains,
  distDir: this.distDir,
  serverComponents: this.enabledDirectories.app,
  cacheLifeProfiles: this.nextConfig.cacheLife,
  staticPageGenerationTimeout: this.nextConfig.staticPageGenerationTimeout,
  enableTainting: this.nextConfig.experimental.taint,
  crossOrigin: this.nextConfig.crossOrigin
    ? this.nextConfig.crossOrigin
    : undefined,
  largePageDataBytes: this.nextConfig.experimental.largePageDataBytes,
 
  isExperimentalCompile: options.compileMode
    ? options.compileMode.isExperimentalCompile
    : this.nextConfig.experimental.isExperimentalCompile,

Sources: packages/next/src/server/base-server.ts:584-611

NextNodeServer Specialization and Execution

Node.js Platform Server Implementation and Request Normalization

NextNodeServer extends BaseServer to implement the Node.js production server runtime, inheriting platform-specific request routing, file system caching bindings, and custom server handlers. The server initializes request adapters that bridge incoming Node.js IncomingMessage and ServerResponse streams into NodeNextRequest and NodeNextResponse wrappers.

protected normalizeReq(
  req: NodeNextRequest | IncomingMessage
): NodeNextRequest {
  return !(req instanceof NodeNextRequest) ? new NodeNextRequest(req) : req
}
 
protected normalizeRes(
  res: NodeNextResponse | ServerResponse
): NodeNextResponse {
  return !(res instanceof NodeNextResponse) ? new NodeNextResponse(res) : res
}

Sources: packages/next/src/server/next-server.ts:1339-1349

The request handler pipeline ensures that prepare() is fired eagerly upon request receipt while catching unhandled rejections prior to execution. When getRequestHandler() or getRequestHandlerWithMetadata() is invoked, incoming parameters and metadata are bound to request attributes via setRequestMeta before routing through the parent handler.

public getRequestHandler(): NodeRequestHandler {
  const handler = this.makeRequestHandler()
  if (this.serverOptions.experimentalTestProxy) {
    const { wrapRequestHandlerNode } =
      // eslint-disable-next-line @next/internal/typechecked-require -- experimental/testmode is not built ins next/dist/esm
      require('next/dist/experimental/testmode/server') as typeof import('../experimental/testmode/server')
    return wrapRequestHandlerNode(handler)
  }
  return handler
}
 
/**
 * @internal - this method is internal to Next.js and should not be used directly by end-users
 */
public getRequestHandlerWithMetadata(meta: RequestMeta): NodeRequestHandler {
  const handler = this.makeRequestHandler()
  return (req, res, parsedUrl) => {
    setRequestMeta(req, meta)
    return handler(req, res, parsedUrl)
  }
}

Sources: packages/next/src/server/next-server.ts:1351-1371

Warning

Calling ensurePage directly on NextNodeServer outside of development environments throws an immediate invariant error, as on-demand compilation is restricted to development server instances.

  throw new Error(
    'Invariant: ensurePage can only be called on the development server'
  )
}

Sources: packages/next/src/server/next-server.ts:1315-1318

Caching Bridges and Incremental Cache Configuration

NextNodeServer manages caching bridges by integrating custom cache handlers and delegating file system operations to nodeFs. Custom cache handlers specified in the user configuration are dynamically imported via dynamicImportEsmDefault, interoped, and registered using setCacheHandler.

  const { cacheMaxMemorySize, cacheHandlers } = this.nextConfig
  if (!cacheHandlers) return
 
  initializeCacheHandlers(cacheMaxMemorySize)
  await registerCustomCacheHandlers(async () => {
    for (const [kind, handler] of Object.entries(cacheHandlers)) {
      if (!handler) continue
 
      setCacheHandler(
        kind,
        interopDefault(
          await dynamicImportEsmDefault(
            formatDynamicImportPath(this.distDir, handler)
          )
        )
      )
    }
  })
}

Sources: packages/next/src/server/next-server.ts:431-449

The incremental cache setup method inspects this.nextConfig for a cacheHandler property, loads any custom handlers asynchronously, and returns the configured cache instance for downstream rendering workers.

protected async getIncrementalCache({
  requestHeaders,
}: {
  requestHeaders: IncrementalCache['requestHeaders']
}) {
  const dev = !!this.dev
  let CacheHandler: any
  const { cacheHandler } = this.nextConfig
 
  if (cacheHandler) {
    CacheHandler = interopDefault(
      await dynamicImportEsmDefault(
        formatDynamicImportPath(this.distDir, cacheHandler)
      )
    )
  }
 
  await this.loadCustomCacheHandlers()

Sources: packages/next/src/server/next-server.ts:451-468

Method / UtilityTarget Platform / ModulePurpose
getCacheFilesystem()CacheFs (nodeFs)Supplies the Node.js file system interface for cache persistence
normalizeReq()NodeNextRequestWraps raw Node.js IncomingMessage objects into Next.js base request streams
normalizeRes()NodeNextResponseWraps raw Node.js ServerResponse objects into Next.js base response streams
handleApiRequest()PagesAPIRouteMatchDispatches legacy Pages API requests via runApi execution
protected async handleApiRequest(
  req: NodeNextRequest,
  res: NodeNextResponse,
  query: ParsedUrlQuery,
  match: PagesAPIRouteMatch
): Promise<boolean> {
  return this.runApi(req, res, query, match)
}
 
protected getCacheFilesystem(): CacheFs {
  return nodeFs
}

Sources: packages/next/src/server/next-server.ts:1326-1337

HTTP Abstractions and Request Adaptation

BaseNextRequest and BaseNextResponse Abstractions

Next.js decouples core request handling from platform-specific APIs by defining abstract base classes: BaseNextRequest and BaseNextResponse. The BaseNextRequest abstract class manages request properties including method, URL, body, and headers, while providing a lazy cookie parser utilizing getCookieParser configured with request headers.

export abstract class BaseNextRequest<Body = any> {
  protected _cookies: NextApiRequestCookies | undefined
  public abstract headers: IncomingHttpHeaders
  public abstract fetchMetrics: FetchMetric[] | undefined
 
  constructor(
    public method: string,
    public url: string,
    public body: Body
  ) {}
 
  // Utils implemented using the abstract methods above
 
  public get cookies() {
    if (this._cookies) return this._cookies
    return (this._cookies = getCookieParser(this.headers)())
  }
}

Sources: packages/next/src/server/base-http/index.ts:28-45

BaseNextResponse defines abstract interfaces for status codes, headers, body setting, and response dispatching. It includes helper methods like redirect, which automatically sets the Location header, status code, and a fallback Refresh header if the status code is a permanent redirect.

export abstract class BaseNextResponse<Destination = any> {
  abstract statusCode: number | undefined
  abstract statusMessage: string | undefined
  abstract get sent(): boolean
 
  constructor(public destination: Destination) {}
 
  /**
   * Sets a value for the header overwriting existing values
   */
  abstract setHeader(name: string, value: string | string[]): this
 
  /**
   * Removes a header
   */
  abstract removeHeader(name: string): this
 
  /**
   * Appends value for the given header name
   */
  abstract appendHeader(name: string, value: string): this
 
  /**
   * Get all values for a header as an array or undefined if no value is present
   */
  abstract getHeaderValues(name: string): string[] | undefined
 
  abstract hasHeader(name: string): boolean
 
  /**
   * Get values for a header concatenated using `,` or undefined if no value is present
   */
  abstract getHeader(name: string): string | undefined
 
  abstract getHeaders(): OutgoingHttpHeaders
 
  abstract body(value: string): this
 
  abstract send(): void
 
  abstract onClose(callback: () => void): void
 
  // Utils implemented using the abstract methods above
 
  public redirect(destination: string, statusCode: number) {
    this.setHeader('Location', destination)
    this.statusCode = statusCode
 
    // Since IE11 doesn't support the 308 header add backwards
    // compatibility using refresh header
    if (statusCode === RedirectStatusCode.PermanentRedirect) {
      this.setHeader('Refresh', `0;url=${destination}`)
    }
 
    return this
  }

Sources: packages/next/src/server/base-http/index.ts:47-102

Node.js Platform Stream Implementation

The NodeNextRequest and NodeNextResponse classes extend the base abstractions to wrap Node.js IncomingMessage and ServerResponse objects. NodeNextRequest exposes a stream() method that converts the underlying Node.js readable stream into a Web ReadableStream, ensuring the stream can only be read once.

export class NodeNextRequest extends BaseNextRequest<Readable> {
  public headers = this._req.headers
  public fetchMetrics: FetchMetric[] | undefined = this._req?.fetchMetrics;
 
  [NEXT_REQUEST_META]: RequestMeta = this._req[NEXT_REQUEST_META] || {}
 
  constructor(private _req: Req) {
    super(_req.method!.toUpperCase(), _req.url!, _req)
  }
 
  get originalRequest() {
    // Need to mimic these changes to the original req object for places where we use it:
    // render.tsx, api/ssg requests
    this._req[NEXT_REQUEST_META] = this[NEXT_REQUEST_META]
    this._req.url = this.url
    this._req.cookies = this.cookies
    return this._req
  }
 
  set originalRequest(value: Req) {
    this._req = value
  }
 
  private streaming = false
 
  /**
   * Returns the request body as a Web Readable Stream. The body here can only
   * be read once as the body will start flowing as soon as the data handler
   * is attached.
   *
   * @internal
   */
  public stream() {
    if (this.streaming) {
      throw new Error(
        'Invariant: NodeNextRequest.stream() can only be called once'
      )
    }
    this.streaming = true
 
    return new ReadableStream({
      start: (controller) => {
        this._req.on('data', (chunk) => {
          controller.enqueue(new Uint8Array(chunk))
        })
        this._req.on('end', () => {
          controller.close()
        })
        this._req.on('error', (err) => {
          controller.error(err)
        })
      },
    })
  }
}

Sources: packages/next/src/server/base-http/node.ts:19-73

Warning

Calling NodeNextRequest.stream() more than once triggers an invariant error stating that the stream can only be called once, preventing multiple consumers from reading a consumed request body.

public stream() {
  if (this.streaming) {
    throw new Error(
      'Invariant: NodeNextRequest.stream() can only be called once'
    )
  }
  this.streaming = true

Sources: packages/next/src/server/base-http/node.ts:51-57

NodeNextResponse delegates header manipulation, status code setting, and response finalization directly to the underlying Node.js ServerResponse instance.

export class NodeNextResponse extends BaseNextResponse<Writable> {
  private textBody: string | undefined = undefined
 
  public [SYMBOL_CLEARED_COOKIES]?: boolean
 
  get originalResponse() {
    if (SYMBOL_CLEARED_COOKIES in this) {
      this._res[SYMBOL_CLEARED_COOKIES] = this[SYMBOL_CLEARED_COOKIES]
    }
 
    return this._res
  }
 
  constructor(
    private _res: ServerResponse & { [SYMBOL_CLEARED_COOKIES]?: boolean }
  ) {
    super(_res)
  }
 
  get sent() {
    return this._res.finished || this._res.headersSent
  }
 
  get statusCode() {
    return this._res.statusCode
  }
 
  set statusCode(value: number) {
    this._res.statusCode = value
  }
 
  get statusMessage() {
    return this._res.statusMessage
  }
 
  set statusMessage(value: string) {
    this._res.statusMessage = value
  }
 
  setHeader(name: string, value: string | string[]): this {
    this._res.setHeader(name, value)
    return this
  }
 
  removeHeader(name: string): this {
    this._res.removeHeader(name)
    return this
  }
 
  getHeaderValues(name: string): string[] | undefined {
    const values = this._res.getHeader(name)
 
    if (values === undefined) return undefined
 
    return (Array.isArray(values) ? values : [values]).map((value) =>
      value.toString()
    )
  }
 
  hasHeader(name: string): boolean {
    return this._res.hasHeader(name)
  }
 
  getHeader(name: string): string | undefined {
    const values = this.getHeaderValues(name)
    return Array.isArray(values) ? values.join(',') : undefined
  }
 
  getHeaders(): OutgoingHttpHeaders {
    return this._res.getHeaders()
  }
 
  appendHeader(name: string, value: string): this {
    const currentValues = this.getHeaderValues(name) ?? []
 
    if (!currentValues.includes(value)) {
      this._res.setHeader(name, [...currentValues, value])
    }
 
    return this

Sources: packages/next/src/server/base-http/node.ts:75-169

Bridging to Web NextRequest via NextRequestAdapter

NextRequestAdapter transforms BaseNextRequest instances into standard NextRequest objects depending on the active execution runtime environment. When running outside the edge runtime, fromNodeNodeNextRequest resolves relative URLs against request metadata and configures the request with a half-duplex body and an abort signal tied to the underlying response lifecycle.

export class NextRequestAdapter {
  public static fromBaseNextRequest(
    request: BaseNextRequest,
    signal: AbortSignal
  ): NextRequest {
    if (
      // The type check here ensures that `req` is correctly typed, and the
      // environment variable check provides dead code elimination.
      process.env.NEXT_RUNTIME === 'edge' &&
      isWebNextRequest(request)
    ) {
      return NextRequestAdapter.fromWebNextRequest(request)
    } else if (
      // The type check here ensures that `req` is correctly typed, and the
      // environment variable check provides dead code elimination.
      process.env.NEXT_RUNTIME !== 'edge' &&
      isNodeNextRequest(request)
    ) {
      return NextRequestAdapter.fromNodeNextRequest(request, signal)
    } else {
      throw new Error('Invariant: Unsupported NextRequest type')
    }
  }
 
  public static fromNodeNextRequest(
    request: NodeNextRequest,
    signal: AbortSignal
  ): NextRequest {
    // HEAD and GET requests can not have a body.
    let body: BodyInit | null = null
    if (request.method !== 'GET' && request.method !== 'HEAD' && request.body) {
      // @ts-expect-error - this is handled by undici, when streams/web land use it instead
      body = request.body
    }
 
    let url: URL
    if (request.url.startsWith('http')) {
      url = new URL(request.url)
    } else {
      // Grab the full URL from the request metadata.
      const base = getRequestMeta(request, 'initURL')
      if (!base || !base.startsWith('http')) {
        // Because the URL construction relies on the fact that the URL provided
        // is absolute, we need to provide a base URL. We can't use the request
        // URL because it's relative, so we use a dummy URL instead.
        url = new URL(request.url, 'http://n')
      } else {
        url = new URL(request.url, base)
      }
    }
 
    return new NextRequest(url, {
      method: request.method,
      headers: fromNodeOutgoingHttpHeaders(request.headers),
      duplex: 'half',
      signal,
      // geo
      // ip
      // nextConfig
 
      // body can not be passed if request was aborted
      // or we get a Request body was disturbed error
      ...(signal.aborted
        ? {}
        : {
            body,
          }),
    })
  }

Sources: packages/next/src/server/web/spec-extension/adapters/next-request.ts:56-124

Important

If a request signal has already been aborted, NextRequestAdapter omits the request body entirely from the constructed NextRequest to prevent disturbing or consuming an aborted body stream.

  // body can not be passed if request was aborted
  // or we get a Request body was disturbed error
  ...(signal.aborted
    ? {}
    : {
        body,
      }),
})

Sources: packages/next/src/server/web/spec-extension/adapters/next-request.ts:116-123

Adapter MethodTarget Input TypeOutput TypePurpose
fromBaseNextRequest()BaseNextRequestNextRequestRuntime-dispatched entrypoint routing to node or web adapters
fromNodeNextRequest()NodeNextRequestNextRequestConverts Node.js incoming messages and metadata into standard NextRequest
fromWebNextRequest()WebNextRequestNextRequestAdapts web-native request wrappers for standard spec consumption
export class NextRequestAdapter {
  public static fromBaseNextRequest(
    request: BaseNextRequest,
    signal: AbortSignal
  ): NextRequest {
    if (
      // The type check here ensures that `req` is correctly typed, and the
      // environment variable check provides dead code elimination.
      process.env.NEXT_RUNTIME === 'edge' &&
      isWebNextRequest(request)
    ) {
      return NextRequestAdapter.fromWebNextRequest(request)
    } else if (
      // The type check here ensures that `req` is correctly typed, and the
      // environment variable check provides dead code elimination.
      process.env.NEXT_RUNTIME !== 'edge' &&
      isNodeNextRequest(request)
    ) {
      return NextRequestAdapter.fromNodeNextRequest(request, signal)
    } else {
      throw new Error('Invariant: Unsupported NextRequest type')
    }
  }

Sources: packages/next/src/server/web/spec-extension/adapters/next-request.ts:56-78

Server Error Handling and Normalization

Error Types and Safe Stringification

Next.js defines the NextError interface extending standard errors to include optional properties like type, page, code, cancelled, and digest. The isError utility validates whether an unknown thrown value is an object containing name and message properties.

export interface NextError extends Error {
  type?: string
  page?: string
  code?: string | number
  cancelled?: boolean
  digest?: string
}

Sources: packages/next/src/lib/is-error.ts:4-10

export default function isError(err: unknown): err is NextError {
  return (
    typeof err === 'object' && err !== null && 'name' in err && 'message' in err
  )
}

Sources: packages/next/src/lib/is-error.ts:38-42

The safeStringifyLite helper serializes objects without requiring external production dependencies by tracking circular references with a WeakSet and replacing duplicates with "[Circular]".

function safeStringifyLite(obj: any) {
  const seen = new WeakSet()
 
  return JSON.stringify(obj, (_key, value) => {
    // If value is an object and already seen, replace with "[Circular]"
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) {
        return '[Circular]'
      }
      seen.add(value)
    }
    return value
  })
}

Sources: packages/next/src/lib/is-error.ts:19-32

Error Normalization and Development Diagnostics

The getProperError function normalizes thrown values into standard Error instances. In development mode, throwing undefined or null triggers explicit errors referencing documentation URLs for diagnostics.

export function getProperError(err: unknown): Error {
  if (isError(err)) {
    return err
  }
 
  if (process.env.NODE_ENV === 'development') {
    // provide better error for case where `throw undefined`
    // is called in development
    if (typeof err === 'undefined') {
      return new Error(
        'An undefined error was thrown, ' +
          'see here for more info: https://nextjs.org/docs/messages/threw-undefined'
      )
    }
 
    if (err === null) {
      return new Error(
        'A null error was thrown, ' +
          'see here for more info: https://nextjs.org/docs/messages/threw-undefined'
      )
    }
  }

Sources: packages/next/src/lib/is-error.ts:44-65

Warning

Plain objects passed as thrown values are converted to JSON strings using safeStringifyLite, while primitive values are coerced into strings before being wrapped in an Error.

return new Error(isPlainObject(err) ? safeStringifyLite(err) : err + '')

Sources: packages/next/src/lib/is-error.ts:67-67

Helper FunctionInput TypeOutput TypePurpose
isError()unknownerr is NextErrorType guard checking for object structure with name and message
getProperError()unknownErrorNormalizes non-error throwables, handling undefined, null, and plain objects
safeStringifyLite()anystringSafe JSON serializer guarding against circular references using a WeakSet
function safeStringifyLite(obj: any) {
  const seen = new WeakSet()
 
  return JSON.stringify(obj, (_key, value) => {
    // If value is an object and already seen, replace with "[Circular]"
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) {
        return '[Circular]'
      }
      seen.add(value)
    }
    return value
  })
}
 
/**
 * Checks whether the given value is a NextError.
 * This can be used to print a more detailed error message with properties like `code` & `digest`.
 */
export default function isError(err: unknown): err is NextError {
  return (
    typeof err === 'object' && err !== null && 'name' in err && 'message' in err
  )
}

Sources: packages/next/src/lib/is-error.ts:19-42

Was this page helpful?