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

Async Storage

Last updated Oct 12, 2026100% grounded
Relevant source files

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

Overview

Next.js uses asynchronous storage infrastructure built on AsyncLocalStorage to propagate request-scoped execution contexts, render metadata, and dynamic states across asynchronous boundaries. By maintaining realm-safe singletons via global symbols and fallback shims, the system tracks active rendering phases, request headers, cookies, and draft mode parameters across Node.js servers, worker threads, and the Edge Runtime. These storage contexts coordinate request lifecycles throughout Server Architecture, managing dynamic data access enforcement and execution boundaries for App Rendering and Route Handlers.

Sources: packages/next/src/server/app-render/async-local-storage.ts:36-83, packages/next/src/server/app-render/work-unit-async-storage.external.ts:28-34, packages/next/src/server/request/cookies.ts:36-147

Global Storage Infrastructure

Global AsyncLocalStorage Singletons

Next.js initializes and exports several core AsyncLocalStorage singletons to manage request contexts, action execution, console logging interception, dynamic access tracking, and background tasks. Each storage instance is created by calling getOrCreateGlobalAsyncLocalStorage with a unique identifier string combined with the current Next.js version (process.env.__NEXT_VERSION) via Symbol.for.

export function getOrCreateGlobalAsyncLocalStorage<Store extends {}>(
  name: string
): AsyncLocalStorage<Store> {
  const key = Symbol.for(`@next/${name}@${process.env.__NEXT_VERSION}`)
 
  const globalStore = globalThis as typeof globalThis & {
    [key: symbol]: AsyncLocalStorage<Store> | undefined
  }
 
  return (globalStore[key] ??= createAsyncLocalStorage<Store>())
}

Sources: packages/next/src/server/app-render/async-local-storage.ts:73-83

Singleton Instance NameStorage Identifier StringAssociated Type
actionAsyncStorageInstance'action-async-storage'ActionAsyncStorage
afterTaskAsyncStorageInstance'after-task-async-storage'AfterTaskAsyncStorage
consoleAsyncStorageInstance'console-async-storage'ConsoleAsyncStorage
dynamicAccessAsyncStorageInstance'dynamic-access-async-storage'DynamicAccessStorage
workAsyncStorageInstance'work-async-storage'WorkAsyncStorage
workUnitAsyncStorageInstance'work-unit-async-storage'WorkUnitAsyncStorage

Sources: packages/next/src/server/app-render/after-task-async-storage-instance.ts:4-5, packages/next/src/server/app-render/action-async-storage-instance.ts:4-5, packages/next/src/server/app-render/console-async-storage-instance.ts:4-5, packages/next/src/server/app-render/dynamic-access-async-storage-instance.ts:4-5, packages/next/src/server/app-render/work-async-storage-instance.ts:4-5, packages/next/src/server/app-render/work-unit-async-storage-instance.ts:4-5

Note

Using Symbol.for with the Next.js version ensures that if multiple distinct versions of Next.js are evaluated within the same JavaScript realm, their async storage instances remain isolated, preventing incompatible store shape collisions.

Sources: packages/next/src/server/app-render/async-local-storage.ts:69-72

Fallback Shims and Environment Safety

When AsyncLocalStorage is unavailable in the execution environment, createAsyncLocalStorage falls back to FakeAsyncLocalStorage. The fake shim implements the AsyncLocalStorage interface by returning undefined for getStore() and throwing an invariant error for all lifecycle operations (disable, run, exit, enterWith).

class FakeAsyncLocalStorage<Store extends {}>
  implements AsyncLocalStorage<Store>
{
  disable(): void {
    throw sharedAsyncLocalStorageNotAvailableError
  }
 
  getStore(): Store | undefined {
    // This fake implementation of AsyncLocalStorage always returns `undefined`.
    return undefined
  }
 
  run<R>(): R {
    throw sharedAsyncLocalStorageNotAvailableError
  }
 
  exit<R>(): R {
    throw sharedAsyncLocalStorageNotAvailableError
  }
 
  enterWith(): void {
    throw sharedAsyncLocalStorageNotAvailableError
  }
 
  static bind<T>(fn: T): T {
    return fn
  }
}

Sources: packages/next/src/server/app-render/async-local-storage.ts:7-34

export function createAsyncLocalStorage<
  Store extends {},
>(): AsyncLocalStorage<Store> {
  if (maybeGlobalAsyncLocalStorage) {
    return new maybeGlobalAsyncLocalStorage()
  }
  return new FakeAsyncLocalStorage()
}

Sources: packages/next/src/server/app-render/async-local-storage.ts:39-46

Warning

Module loader symlink resolution bugs (such as those tied to fs.realpathSync in next dev under package managers like pnpm) can cause the same file to be evaluated multiple times in a single realm, creating duplicate module instances. Storing singletons on globalThis via symbol keys prevents context loss across these duplicate module evaluations.

Sources: packages/next/src/server/app-render/async-local-storage.ts:50-68

For client-side compilation, server-async-storage.ts is aliased to server-async-storage.browser.ts, exporting undefined stubs for actionAsyncStorage, workAsyncStorage, and workUnitAsyncStorage to prevent server-only modules from leaking into browser bundles.

// Browser variant of `./server-async-storage`. The server-only async-storage
// singletons don't exist in the browser, so these are `undefined`. Consumers
// guard usage behind `typeof window === 'undefined'`, so the stubs are never
// dereferenced.
export const actionAsyncStorage = undefined
export const workAsyncStorage = undefined
export const workUnitAsyncStorage = undefined

Sources: packages/next/src/client/components/server-async-storage.browser.ts:1-7, packages/next/src/client/components/server-async-storage.ts:1-9

Work Store Lifecycle

Store Creation and Prerender Configuration

The WorkStore is instantiated through factory functions that construct and initialize store instances based on rendering contexts and build options. The createWorkStore and createPrerenderWorkStore functions invoke createWorkStoreImpl, passing parameters that dictate fetch metrics tracking behavior.

Factory FunctionMetric Tracking Flag Condition
createWorkStore!!process.env.__NEXT_DEV_SERVER
createPrerenderWorkStore!!process.env.__NEXT_DEV_SERVER || !!process.env.NEXT_DEBUG_BUILD || process.env.NEXT_SSG_FETCH_METRICS === '1'

Sources: packages/next/src/server/async-storage/work-store.ts:91-102

Initialization and Context Capture

During initialization, createWorkStoreImpl maps properties from WorkStoreContext and RenderOpts directly onto the WorkStore object. It normalizes the route using normalizeAppPath(page), resolves the incremental cache by falling back to (globalThis as any).__incrementalCache when edge runtime local environments lack an explicit instance, and records the request start time using performance.timeOrigin + performance.now().

const store: WorkStore = {
  page,
  route: normalizeAppPath(page),
  incrementalCache:
    // we fallback to a global incremental cache for edge-runtime locally
    // so that it can access the fs cache without mocks
    renderOpts.incrementalCache || (globalThis as any).__incrementalCache,
  cacheLifeProfiles: renderOpts.cacheLifeProfiles,
  useCacheTimeout: renderOpts.experimental.useCacheTimeout,
  durableUseCacheEntries: renderOpts.experimental.durableUseCacheEntries,
  staticPageGenerationTimeout: renderOpts.staticPageGenerationTimeout,
  isBuildTimePrerendering: renderOpts.isBuildTimePrerendering,
  fetchCache: renderOpts.fetchCache,
  isOnDemandRevalidate: renderOpts.isOnDemandRevalidate,
  requestId: undefined,
  htmlRequestId: undefined,
 
  isDraftMode: renderOpts.isDraftMode,
 
  isPrefetchRequest,
  buildId,
  deploymentId,
  reactLoadableManifest: renderOpts?.reactLoadableManifest || {},
  assetPrefix: renderOpts?.assetPrefix || '',
  nonce,
 
  afterContext: createAfterContext(renderOpts),
  cacheComponentsEnabled: renderOpts.cacheComponents,
  validationLevel: renderOpts.validationLevel,
  previouslyRevalidatedTags,
  requestStartTime: performance.timeOrigin + performance.now(),
  refreshTagsByCacheKind: createRefreshTagsByCacheKind(),
  runInCleanSnapshot: createSnapshot(),
  shouldTrackFetchMetrics,
  clientComponentLoadTracker: undefined,
  reactServerErrorsByDigest: new Map(),
}

Sources: packages/next/src/server/async-storage/work-store.ts:116-152

Note

renderOpts.store is temporarily assigned the newly created store reference to support legacy code paths accessing store values outside the active request execution context.

Sources: packages/next/src/server/async-storage/work-store.ts:154-156

After-Context and Lazy Tag Refreshing

The lifecycle builder constructs an AfterContext instance using lifecycle options such as waitUntil, onClose, and onTaskError. Simultaneously, it initializes the refreshTagsByCacheKind map by iterating over available cache handler entries and wrapping handlers that expose a refreshTags method into LazyResult instances.

function createAfterContext(renderOpts: RequestLifecycleOpts): AfterContext {
  const { waitUntil, onClose, onAfterTaskError } = renderOpts
  return new AfterContext({
    waitUntil,
    onClose,
    onTaskError: onAfterTaskError,
  })
}
 
/**
 * Creates a map with lazy results that refresh tags for the respective cache
 * kind when they're awaited for the first time.
 */
function createRefreshTagsByCacheKind(): Map<string, LazyResult<void>> {
  const refreshTagsByCacheKind = new Map<string, LazyResult<void>>()
  const cacheHandlers = getCacheHandlerEntries()
 
  if (cacheHandlers) {
    for (const [kind, cacheHandler] of cacheHandlers) {
      if ('refreshTags' in cacheHandler) {
        refreshTagsByCacheKind.set(
          kind,
          createLazyResult(async () => cacheHandler.refreshTags())
        )
      }
    }
  }
 
  return refreshTagsByCacheKind
}

Sources: packages/next/src/server/async-storage/work-store.ts:160-189

Static Generation Snapshotting

Static generation and cache entry creation require isolation from request-specific state. The runInCleanSnapshot property on the store is initialized via createSnapshot(), supplying a clean AsyncLocalStorage snapshot wrapper that prevents cache generation routines from reading request-bound scopes like cookies() inside React.cache().

refreshTagsByCacheKind: createRefreshTagsByCacheKind(),
runInCleanSnapshot: createSnapshot(),

Sources: packages/next/src/server/async-storage/work-store.ts:147-148, packages/next/src/server/app-render/work-async-storage.external.ts:197-200

Work Unit Store Hierarchy

Common Work Unit Stores and Phase Management

Work unit stores represent different execution scopes in the App Router, separating active requests, prerenders, and background tasks. Every work unit store extends CommonWorkUnitStore, which defines a mutable phase property and readonly implicitTags.

Work Unit PhaseDescription
'action'Active execution of Server Actions responding to client mutations
'render'Server-side rendering of Server Component trees and route segments
'after'Execution of deferred work via background tasks or after callbacks

Sources: packages/next/src/server/app-render/work-unit-async-storage.external.ts:28-34

Request and Prerender Contexts

The RequestStore interface models active HTTP requests and supports mutable cookies, draft mode tracking, header access, and incremental resume data caches. It maintains asynchronous API promises and handles development-only flags like needsRuntimeShell and hasIncompatibleShellContent to distinguish static and runtime prerender behaviors.

export interface RequestStore extends CommonWorkUnitStore {
  readonly type: 'request'
 
  /**
   * The URL of the request. This only specifies the pathname and the search
   * part of the URL.
   */
  readonly url: {
    /**
     * The pathname of the requested URL.
     */
    readonly pathname: string
 
    /**
     * The search part of the requested URL. If the request did not provide a
     * search part, this will be an empty string.
     */
    readonly search: string
  }
 
  readonly headers: ReadonlyHeaders
  // This is mutable because we need to reassign it when transitioning from the action phase to the render phase.
  // The cookie object itself is deliberately read only and thus can't be updated.
  cookies: ReadonlyRequestCookies
  readonly mutableCookies: ResponseCookies
  readonly userspaceMutableCookies: ResponseCookies
  readonly draftMode: DraftModeProvider
  readonly isHmrRefresh?: boolean
  readonly serverComponentsHmrCache?: ServerComponentsHmrCache
  readonly hmrRefreshHash?: string
 
  readonly rootParams: Params
 
  /**
   * The resume data cache for this request. Either a mutable
   * `PrerenderResumeDataCache` (e.g. during a dev warmup that fills caches) or
   * an immutable `RenderResumeDataCache` (e.g. when resuming from a postponed
   * state). Narrow via `resumeDataCache.mutable` to tell them apart.
   */
  resumeDataCache: ResumeDataCache | null
 

Sources: packages/next/src/server/app-render/work-unit-async-storage.external.ts:36-76

Note

PrerenderStoreModern covers client, server, and runtime prerendering modes along with validation stores, utilizing conditional properties to track cache population without explicitly branching RSC and SSR code paths.

Sources: packages/next/src/server/app-render/work-unit-async-storage.external.ts:149-154

Specialized Asynchronous Storages

Action execution, background tasks, and dynamic access tracking operate through dedicated AsyncLocalStorage instances. ActionStore tracks whether execution is part of an action or an App Route handler. AfterTaskStore captures the rootTaskSpawnphase corresponding to the phase in which the topmost after call was invoked.

export interface ActionStore {
  readonly isAction?: boolean
  readonly isAppRoute?: boolean
}

Sources: packages/next/src/server/app-render/action-async-storage.external.ts:5-8, packages/next/src/server/app-render/after-task-async-storage.external.ts:7-14

Dynamic access tracking relies on DynamicAccessAsyncStore, which holds an AbortController and a reason restricted to 'fallback-params' or 'runtime'. The abortOnDynamicAccess function updates the store reason unless it is already marked as 'runtime' and aborts the associated controller.

export interface DynamicAccessAsyncStore {
  readonly abortController: AbortController
  reason: DynamicAccessReason | null
}
 
export type DynamicAccessReason = 'fallback-params' | 'runtime'

Sources: packages/next/src/server/app-render/dynamic-access-async-storage.external.ts:6-11, packages/next/src/server/app-render/dynamic-access-async-storage.external.ts:16-29

Warning

Cache evaluation routines reading both fallback and runtime parameters will upgrade the cancellation reason from fallback-only to runtime, preventing stale static optimizations when runtime data is touched.

Sources: packages/next/src/server/app-render/dynamic-access-async-storage.external.ts:21-28

Request Store Composition

Request Store Builders and Inputs

The construction of a RequestStore instance relies on specialized creator functions that initialize phase contexts, route parameters, request headers, cookies, and cache references. createRequestStoreForRender initializes a store with a 'render' phase by default, whereas createRequestStoreForAPI defaults to an 'action' phase with empty root parameters ({}) and null resume data caches. Both helper functions delegate initialization to the core createRequestStore function, which accepts a RequestStoreInputs descriptor containing raw incoming request properties and optional HMR or preview properties.

Creator FunctionDefault PhaseRoot ParametersResume Data Cache
createRequestStoreForRender'render'rootParams (passed)resumeDataCache
createRequestStoreForAPI'action'{} (empty)null

Sources: packages/next/src/server/async-storage/request-store.ts:184-244

Note

Side-process consumers, such as the 'use cache' deadlock probe worker, invoke createRequestStore directly with a serializable snapshot of headers and metadata when a native req/res pair is absent.

Sources: packages/next/src/server/async-storage/request-store.ts:246-253

Caching and Lazy Initialization

The RequestStore object uses internal lazy getters backed by a private cache object to construct and memoize expensive adapter instances on first access. Properties including headers, cookies, mutableCookies, userspaceMutableCookies, and draftMode are instantiated on demand, preventing unnecessary parsing overhead for requests that do not touch them.

const cache: {
  headers?: ReadonlyHeaders
  cookies?: ReadonlyRequestCookies
  mutableCookies?: ResponseCookies
  userspaceMutableCookies?: ResponseCookies
  draftMode?: DraftModeProvider
} = {}
 
return {
  type: 'request',
  phase,
  implicitTags,
  // Rather than just using the whole `url` here, we pull the parts we want
  // to ensure we don't use parts of the URL that we shouldn't. This also
  // lets us avoid requiring an empty string for `search` in the type.
  url: { pathname: url.pathname, search: url.search ?? '' },
  rootParams,
  get headers() {
    if (!cache.headers) {
      // Seal the headers object that'll freeze out any methods that could
      // mutate the underlying data.
      cache.headers = getHeaders(headers)
    }
 
    return cache.headers
  },
  get cookies() {
    if (!cache.cookies) {
      // if middleware is setting cookie(s), then include those in
      // the initial cached cookies so they can be read in render
      const requestCookies = new RequestCookies(HeadersAdapter.from(headers))
 
      mergeMiddlewareCookies(headers, requestCookies)
 
      // Seal the cookies object that'll freeze out any methods that could
      // mutate the underlying data.
      cache.cookies = RequestCookiesAdapter.seal(requestCookies)
    }
 
    return cache.cookies
  },
  set cookies(value: ReadonlyRequestCookies) {
    cache.cookies = value
  },
  get mutableCookies() {
    if (!cache.mutableCookies) {
      const mutableCookies = getMutableCookies(headers, onUpdateCookies)
 
      mergeMiddlewareCookies(headers, mutableCookies)
 
      cache.mutableCookies = mutableCookies
    }
    return cache.mutableCookies
  },
  get userspaceMutableCookies() {
    if (!cache.userspaceMutableCookies) {
      const userspaceMutableCookies =
        createCookiesWithMutableAccessCheck(this)
      cache.userspaceMutableCookies = userspaceMutableCookies
    }
    return cache.userspaceMutableCookies
  },
  get draftMode() {
    if (!cache.draftMode) {
      cache.draftMode = new DraftModeProvider(
        previewProps,
        headers,
        this.cookies,
        this.mutableCookies
      )
    }
 
    return cache.draftMode
  },

Sources: packages/next/src/server/async-storage/request-store.ts:269-342

Cookie initialization parses request cookies from headers and automatically merges any middleware-set cookies present in the 'x-middleware-set-cookie' header via mergeMiddlewareCookies. The sealed cookies property returns a read-only adapter, while mutableCookies provides a ResponseCookies instance capable of tracking modifications and triggering onUpdateCookies callbacks.

get cookies() {
  if (!cache.cookies) {
    // if middleware is setting cookie(s), then include those in
    // the initial cached cookies so they can be read in render
    const requestCookies = new RequestCookies(HeadersAdapter.from(headers))
 
    mergeMiddlewareCookies(headers, requestCookies)
 
    // Seal the cookies object that'll freeze out any methods that could
    // mutate the underlying data.
    cache.cookies = RequestCookiesAdapter.seal(requestCookies)
  }
 
  return cache.cookies
},
set cookies(value: ReadonlyRequestCookies) {
  cache.cookies = value
},
get mutableCookies() {
  if (!cache.mutableCookies) {
    const mutableCookies = getMutableCookies(headers, onUpdateCookies)
 
    mergeMiddlewareCookies(headers, mutableCookies)
 
    cache.mutableCookies = mutableCookies
  }
  return cache.mutableCookies
},

Sources: packages/next/src/server/async-storage/request-store.ts:295-322

Draft Mode and Phase Synchronization

Draft mode evaluation is handled by the DraftModeProvider class, which inspects incoming headers for on-demand revalidation flags and validates the prerender bypass cookie against the configured previewModeId. In non-production environments, previewModeId accepts a 'development-id' fallback match regardless of the exact cookie hash value.

Method / GetterAction Description
isEnabledEvaluates whether the prerender bypass cookie matches the preview ID and on-demand revalidation is absent
enable()Sets the prerender bypass cookie (__prerender_bypass) into mutable cookies with httpOnly: true and path /
disable()Expires the prerender bypass cookie immediately by setting expires: new Date(0)

Sources: packages/next/src/server/async-storage/draft-mode-provider.ts:11-94

Caution

Calling enable() when previewModeId is undefined throws an immediate invariant error because the bypass cookie value cannot be determined.

Sources: packages/next/src/server/async-storage/draft-mode-provider.ts:59-65

When request phases transition or mutable cookies are updated, synchronizeMutableCookies converts updated response cookies back into sealed request cookies so subsequent reads reflect the mutations.

export function synchronizeMutableCookies(store: RequestStore) {
  // TODO: does this need to update headers as well?
  store.cookies = RequestCookiesAdapter.seal(
    responseCookiesToRequestCookies(store.mutableCookies)
  )
}

Sources: packages/next/src/server/async-storage/request-store.ts:354-359

Request Data Access Enforcement

Dynamic Resolution and Bailout Guards

When application code invokes headers(), cookies(), or draftMode(), Next.js resolves the request data context via workAsyncStorage and workUnitAsyncStorage. If either store is missing, execution immediately invokes throwForMissingRequestStore. When a work store is present, calls enforce structural constraints, starting with phase validation: if a workUnitStore is active and isRequestApiAllowedInCurrentPhase evaluates to false, accessing cookies() or headers() inside an after() callback throws an unsupported usage error. If workStore.forceStatic is enabled, all dynamic enforcement logic is bypassed, returning an empty, untracked cookies or headers object. If workStore.dynamicShouldError is true, a StaticGenBailoutError is thrown, aborting static generation.

Sources: packages/next/src/server/request/cookies.ts:36-60, packages/next/src/server/request/headers.ts:43-94

Warning

Accessing cookies() or headers() inside a "use cache" block or an unstable_cache invocation throws specialized cache error messages (createCookiesInUseCacheError, createHeadersInUseCacheError, or their unstable counterparts) to prevent non-deterministic dynamic reads inside cached boundaries.

Sources: packages/next/src/server/request/cookies.ts:61-70, packages/next/src/server/request/headers.ts:62-72

Work Unit Store Resolution Branches

Depending on the specific workUnitStore.type, request data access follows distinct execution branches that determine whether data is tracked, delayed, or rejected.

Work Unit Store Typecookies() Behaviorheaders() Behavior
'cache' / 'unstable-cache'Throws use-cache or unstable-cache errorThrows use-cache or unstable-cache error
'build-time-generator'Throws wrong-context build time errorThrows wrong-context build time error
'prerender'Returns hanging session data promiseReturns hanging session data promise
'prerender-client' / 'validation-client'Throws InvariantError (Client Component usage)Throws InvariantError (Client Component usage)
'prerender-legacy'Interrupts static generation (throwToInterruptStaticGeneration)Interrupts static generation (throwToInterruptStaticGeneration)
'prerender-runtime'Delays via stagedRendering or returns untrackedDelays via stagedRendering or returns untracked
'private-cache'Returns untracked workUnitStore.cookiesReturns untracked workUnitStore.headers
'request'Tracks dynamic data and returns mutable or readonly cookiesTracks dynamic data and returns headers

Sources: packages/next/src/server/request/cookies.ts:61-142, packages/next/src/server/request/headers.ts:62-153

Draft Mode Access and Mutation Tracking

The draftMode() function returns a promise resolving to a DraftMode instance. Accessing .isEnabled or invoking .enable() and .disable() triggers specific enforcement paths. In development environments without a prefetch request, draftMode() returns a proxied promise instrumented with createDraftModeWithDevWarnings that logs errors if properties are accessed synchronously without unwrapping the promise via await or React.use().

function createDraftModeWithDevWarnings(
  underlyingProvider: null | DraftModeProvider,
  route: undefined | string
): Promise<DraftMode> {
  const instance = new DraftMode(underlyingProvider)
  const promise = Promise.resolve(instance)
 
  const proxiedPromise = new Proxy(promise, {
    get(target, prop, receiver) {
      switch (prop) {
        case 'isEnabled':
          warnForSyncAccess(route, `\`draftMode().${prop}\``)
          break
        case 'enable':
        case 'disable': {
          warnForSyncAccess(route, `\`draftMode().${prop}()\``)
          break
        }
        default: {
          // We only warn for well-defined properties of the draftMode object.
        }
      }
 
      return ReflectAdapter.get(target, prop, receiver)
    },
  })
 
  return proxiedPromise
}

Sources: packages/next/src/server/request/draft-mode.ts:118-146

When .enable() or .disable() is called, trackDynamicDraftMode verifies that the current work unit phase is not 'after', throws a StaticGenBailoutError if dynamicShouldError is enabled, and handles cache and prerender boundaries:

  • 'cache' and 'private-cache' throw createDraftModeMutationInUseCacheError.
  • 'unstable-cache' throws createDraftModeMutationInUnstableCacheError.
  • 'prerender' and 'prerender-runtime' trigger an abort and throw synchronous request access errors requiring await connection().
  • 'prerender-legacy' sets workUnitStore.revalidate = 0 and throws a DynamicServerError.

Sources: packages/next/src/server/request/draft-mode.ts:194-271

Context Propagation Across Tasks

Test Modes and Request Context Propagation

Next.js propagates test parameters, proxy routing ports, and test data across asynchronous task boundaries using specialized AsyncLocalStorage instances. The test mode infrastructure defines TestReqInfo objects containing a request URL, proxyPort number, and testData string, extracted from incoming requests via TestRequestReader implementations.

export interface TestReqInfo {
  url: string
  proxyPort: number
  testData: string
}

Sources: packages/next/src/experimental/testmode/context.ts:3-7

The withRequest helper checks for the presence of the next-test-proxy-port header on incoming requests. If found, it runs the provided asynchronous or synchronous callback within the testStorage AsyncLocalStorage context; otherwise, it executes the callback directly.

function extractTestInfoFromRequest<R>(
  req: R,
  reader: TestRequestReader<R>
): TestReqInfo | undefined {
  const proxyPortHeader = reader.header(req, 'next-test-proxy-port')
  if (!proxyPortHeader) {
    return undefined
  }
  const url = reader.url(req)
  const proxyPort = Number(proxyPortHeader)
  const testData = reader.header(req, 'next-test-data') || ''
  return { url, proxyPort, testData }
}
 
export function withRequest<R, T>(
  req: R,
  reader: TestRequestReader<R>,
  fn: () => T
): T {
  const testReqInfo = extractTestInfoFromRequest(req, reader)
  if (!testReqInfo) {
    return fn()
  }
  return testStorage.run(testReqInfo, fn)
}

Sources: packages/next/src/experimental/testmode/context.ts:16-40

To retrieve the current test context, getTestReqInfo checks the active testStorage store first, falling back to an explicit request extraction if optional parameters are provided.

export function getTestReqInfo<R>(
  req?: R,
  reader?: TestRequestReader<R>
): TestReqInfo | undefined {
  const testReqInfo = testStorage.getStore()
  if (testReqInfo) {
    return testReqInfo
  }
  if (req && reader) {
    return extractTestInfoFromRequest(req, reader)
  }
  return undefined
}

Sources: packages/next/src/experimental/testmode/context.ts:42-54

Tracing Spans and Platform Contexts

Request tracing spans and platform-provided request contexts bridge low-level server initialization and runtime execution via dedicated storage layers. Request insights identity tracking (RequestInsightsIdentity) covers the portion of BaseServer request handling that executes before App Render initializes work storage (workAsyncStorage).

export type RequestInsightsIdentity = {
  requestId: string
  kind?: RequestInsightKind
  htmlRequestId: string
  url: string | undefined
}

Sources: packages/next/src/server/lib/trace/request-insights-identity.ts:5-10

This metadata is managed through a global AsyncLocalStorage store instance initialized via request-insights-identity-storage.

// This storage covers the part of BaseServer request handling that runs before
// App Render creates workAsyncStorage. Once available, workStore remains the
// primary identity source for locally recorded spans.
function getRequestInsightsIdentityStorage(): AsyncLocalStorage<RequestInsightsIdentity> {
  return getOrCreateGlobalAsyncLocalStorage('request-insights-identity-storage')
}
 
export function runWithRequestInsightsIdentity<T>(
  identity: RequestInsightsIdentity,
  fn: () => T
): T {
  return getRequestInsightsIdentityStorage().run(identity, fn)
}
 
export function getRequestInsightsIdentity():
  | RequestInsightsIdentity
  | undefined {
  return getRequestInsightsIdentityStorage().getStore()
}

Sources: packages/next/src/server/lib/trace/request-insights-identity.ts:12-30

Similarly, platform-provided request contexts (such as those containing waitUntil promises for deferred execution) are stored on globalThis under the symbol @next/request-context. getBuiltinRequestContext resolves this symbol from global state, while createLocalRequestContext wraps an AsyncLocalStorage instance to expose a .get() and .run() interface matching platform requirements.

export function getBuiltinRequestContext():
  | BuiltinRequestContextValue
  | undefined {
  const _globalThis = globalThis as GlobalThisWithRequestContext
  const ctx = _globalThis[NEXT_REQUEST_CONTEXT_SYMBOL]
  return ctx?.get()
}
 
const NEXT_REQUEST_CONTEXT_SYMBOL = Symbol.for('@next/request-context')
 
type GlobalThisWithRequestContext = typeof globalThis & {
  [NEXT_REQUEST_CONTEXT_SYMBOL]?: BuiltinRequestContext
}
 
/** A request context provided by the platform. */
export type BuiltinRequestContext = {
  get(): BuiltinRequestContextValue | undefined
}
 
export type RunnableBuiltinRequestContext = BuiltinRequestContext & {
  run<T>(value: BuiltinRequestContextValue, callback: () => T): T
}
 
export type BuiltinRequestContextValue = {
  waitUntil?: WaitUntil
}
export type WaitUntil = (promise: Promise<any>) => void
 
/** "@next/request-context" has a different signature from AsyncLocalStorage,
 * matching [AsyncContext.Variable](https://github.com/tc39/proposal-async-context).
 * We don't need a full AsyncContext adapter here, just having `.get()` is enough
 */
export function createLocalRequestContext(): RunnableBuiltinRequestContext {
  const storage = createAsyncLocalStorage<BuiltinRequestContextValue>()
  return {
    get: () => storage.getStore(),
    run: (value, callback) => storage.run(value, callback),
  }
}

Sources: packages/next/src/server/after/builtin-request-context.ts:3-41

Was this page helpful?