next.js/packages/next/src/server/request/cookies.ts
cookies.ts328 lines11.7 KB
import {
  type ReadonlyRequestCookies,
  areCookiesMutableInCurrentPhase,
  RequestCookiesAdapter,
} from '../web/spec-extension/adapters/request-cookies'
import { RequestCookies } from '../web/spec-extension/cookies'
import {
  workAsyncStorage,
  type WorkStore,
} from '../app-render/work-async-storage.external'
import {
  throwForMissingRequestStore,
  workUnitAsyncStorage,
  type PrerenderStoreModern,
  type RequestStore,
  isInEarlyRenderStage,
} from '../app-render/work-unit-async-storage.external'
import {
  postponeWithTracking,
  throwToInterruptStaticGeneration,
  trackDynamicDataInDynamicRender,
} from '../app-render/dynamic-rendering'
import { StaticGenBailoutError } from '../../client/components/static-generation-bailout'
import {
  delayUntilRuntimeStage,
  makeDevtoolsIOAwarePromise,
  makeHangingPromise,
} from '../dynamic-rendering-utils'
import { createDedupedByCallsiteServerErrorLoggerDev } from '../create-deduped-by-callsite-server-error-logger'
import { isRequestAPICallableInsideAfter } from './utils'
import { applyOwnerStack } from '../dynamic-rendering-utils'
import { InvariantError } from '../../shared/lib/invariant-error'
import { RenderStage } from '../app-render/staged-rendering'

export function cookies(): Promise<ReadonlyRequestCookies> {
  const callingExpression = 'cookies'
  const workStore = workAsyncStorage.getStore()
  const workUnitStore = workUnitAsyncStorage.getStore()

  if (workStore) {
    if (
      workUnitStore &&
      workUnitStore.phase === 'after' &&
      !isRequestAPICallableInsideAfter()
    ) {
      throw new Error(
        // TODO(after): clarify that this only applies to pages?
        `Route ${workStore.route} used \`cookies()\` inside \`after()\`. This is not supported. If you need this data inside an \`after()\` callback, use \`cookies()\` outside of the callback. See more info here: https://nextjs.org/docs/app/api-reference/functions/after`
      )
    }

    if (workStore.forceStatic) {
      // When using forceStatic we override all other logic and always just return an empty
      // cookies object without tracking
      const underlyingCookies = createEmptyCookies()
      return makeUntrackedCookies(underlyingCookies)
    }

    if (workStore.dynamicShouldError) {
      throw new StaticGenBailoutError(
        `Route ${workStore.route} with \`dynamic = "error"\` couldn't be rendered statically because it used \`cookies()\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`
      )
    }

    if (workUnitStore) {
      switch (workUnitStore.type) {
        case 'cache':
          const error = new Error(
            `Route ${workStore.route} used \`cookies()\` inside "use cache". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use \`cookies()\` outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache`
          )
          Error.captureStackTrace(error, cookies)
          applyOwnerStack(error)
          workStore.invalidDynamicUsageError ??= error
          throw error
        case 'unstable-cache':
          throw new Error(
            `Route ${workStore.route} used \`cookies()\` inside a function cached with \`unstable_cache()\`. Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use \`cookies()\` outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache`
          )
        case 'generate-static-params':
          throw new Error(
            `Route ${workStore.route} used \`cookies()\` inside \`generateStaticParams\`. This is not supported because \`generateStaticParams\` runs at build time without an HTTP request. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context`
          )
        case 'prerender':
          return makeHangingCookies(workStore, workUnitStore)
        case 'prerender-client':
        case 'validation-client':
          const exportName = '`cookies`'
          throw new InvariantError(
            `${exportName} must not be used within a Client Component. Next.js should be preventing ${exportName} from being included in Client Components statically, but did not in this case.`
          )
        case 'prerender-ppr':
          // We need track dynamic access here eagerly to keep continuity with
          // how cookies has worked in PPR without cacheComponents.
          return postponeWithTracking(
            workStore.route,
            callingExpression,
            workUnitStore.dynamicTracking
          )
        case 'prerender-legacy':
          // We track dynamic access here so we don't need to wrap the cookies
          // in individual property access tracking.
          return throwToInterruptStaticGeneration(
            callingExpression,
            workStore,
            workUnitStore
          )
        case 'prerender-runtime':
          return delayUntilRuntimeStage(
            workUnitStore,
            makeUntrackedCookies(workUnitStore.cookies)
          )
        case 'private-cache':
          // Private caches are delayed until the runtime stage in use-cache-wrapper,
          // so we don't need an additional delay here.
          return makeUntrackedCookies(workUnitStore.cookies)
        case 'request':
          trackDynamicDataInDynamicRender(workUnitStore)

          let underlyingCookies: ReadonlyRequestCookies

          if (areCookiesMutableInCurrentPhase(workUnitStore)) {
            // We can't conditionally return different types here based on the context.
            // To avoid confusion, we always return the readonly type here.
            underlyingCookies =
              workUnitStore.userspaceMutableCookies as unknown as ReadonlyRequestCookies
          } else {
            underlyingCookies = workUnitStore.cookies
          }

          if (process.env.NODE_ENV === 'development') {
            // Semantically we only need the dev tracking when running in `next dev`
            // but since you would never use next dev with production NODE_ENV we use this
            // as a proxy so we can statically exclude this code from production builds.
            return makeUntrackedCookiesWithDevWarnings(
              workUnitStore,
              underlyingCookies,
              workStore?.route
            )
          } else if (workUnitStore.asyncApiPromises) {
            const early = isInEarlyRenderStage(workUnitStore)
            if (underlyingCookies === workUnitStore.mutableCookies) {
              return early
                ? workUnitStore.asyncApiPromises.earlyMutableCookies
                : workUnitStore.asyncApiPromises.mutableCookies
            } else {
              return early
                ? workUnitStore.asyncApiPromises.earlyCookies
                : workUnitStore.asyncApiPromises.cookies
            }
          } else {
            return makeUntrackedCookies(underlyingCookies)
          }
        default:
          workUnitStore satisfies never
      }
    }
  }

  // If we end up here, there was no work store or work unit store present.
  throwForMissingRequestStore(callingExpression)
}

function createEmptyCookies(): ReadonlyRequestCookies {
  return RequestCookiesAdapter.seal(new RequestCookies(new Headers({})))
}

interface CacheLifetime {}
const CachedCookies = new WeakMap<
  CacheLifetime,
  Promise<ReadonlyRequestCookies>
>()

function makeHangingCookies(
  workStore: WorkStore,
  prerenderStore: PrerenderStoreModern
): Promise<ReadonlyRequestCookies> {
  const cachedPromise = CachedCookies.get(prerenderStore)
  if (cachedPromise) {
    return cachedPromise
  }

  const promise = makeHangingPromise<ReadonlyRequestCookies>(
    prerenderStore.renderSignal,
    workStore.route,
    '`cookies()`'
  )
  CachedCookies.set(prerenderStore, promise)

  return promise
}

function makeUntrackedCookies(
  underlyingCookies: ReadonlyRequestCookies
): Promise<ReadonlyRequestCookies> {
  const cachedCookies = CachedCookies.get(underlyingCookies)
  if (cachedCookies) {
    return cachedCookies
  }

  const promise = Promise.resolve(underlyingCookies)
  CachedCookies.set(underlyingCookies, promise)

  return promise
}

function makeUntrackedCookiesWithDevWarnings(
  requestStore: RequestStore,
  underlyingCookies: ReadonlyRequestCookies,
  route?: string
): Promise<ReadonlyRequestCookies> {
  if (requestStore.asyncApiPromises) {
    const early = isInEarlyRenderStage(requestStore)
    let promise: Promise<ReadonlyRequestCookies>
    if (underlyingCookies === requestStore.mutableCookies) {
      promise = early
        ? requestStore.asyncApiPromises.earlyMutableCookies
        : requestStore.asyncApiPromises.mutableCookies
    } else if (underlyingCookies === requestStore.cookies) {
      promise = early
        ? requestStore.asyncApiPromises.earlyCookies
        : requestStore.asyncApiPromises.cookies
    } else {
      throw new InvariantError(
        'Received an underlying cookies object that does not match either `cookies` or `mutableCookies`'
      )
    }
    return instrumentCookiesPromiseWithDevWarnings(promise, route)
  }

  const cachedCookies = CachedCookies.get(underlyingCookies)
  if (cachedCookies) {
    return cachedCookies
  }

  const promise = makeDevtoolsIOAwarePromise(
    underlyingCookies,
    requestStore,
    RenderStage.Runtime
  )

  const proxiedPromise = instrumentCookiesPromiseWithDevWarnings(promise, route)

  CachedCookies.set(underlyingCookies, proxiedPromise)

  return proxiedPromise
}

const warnForSyncAccess = createDedupedByCallsiteServerErrorLoggerDev(
  createCookiesAccessError
)

function instrumentCookiesPromiseWithDevWarnings(
  promise: Promise<ReadonlyRequestCookies>,
  route: string | undefined
) {
  Object.defineProperties(promise, {
    [Symbol.iterator]: replaceableWarningDescriptorForSymbolIterator(
      promise,
      route
    ),
    size: replaceableWarningDescriptor(promise, 'size', route),
    get: replaceableWarningDescriptor(promise, 'get', route),
    getAll: replaceableWarningDescriptor(promise, 'getAll', route),
    has: replaceableWarningDescriptor(promise, 'has', route),
    set: replaceableWarningDescriptor(promise, 'set', route),
    delete: replaceableWarningDescriptor(promise, 'delete', route),
    clear: replaceableWarningDescriptor(promise, 'clear', route),
    toString: replaceableWarningDescriptor(promise, 'toString', route),
  })
  return promise
}

function replaceableWarningDescriptor(
  target: unknown,
  prop: string,
  route: string | undefined
) {
  return {
    enumerable: false,
    get() {
      warnForSyncAccess(route, `\`cookies().${prop}\``)
      return undefined
    },
    set(value: unknown) {
      Object.defineProperty(target, prop, {
        value,
        writable: true,
        configurable: true,
      })
    },
    configurable: true,
  }
}

function replaceableWarningDescriptorForSymbolIterator(
  target: unknown,
  route: string | undefined
) {
  return {
    enumerable: false,
    get() {
      warnForSyncAccess(route, '`...cookies()` or similar iteration')
      return undefined
    },
    set(value: unknown) {
      Object.defineProperty(target, Symbol.iterator, {
        value,
        writable: true,
        enumerable: true,
        configurable: true,
      })
    },
    configurable: true,
  }
}

function createCookiesAccessError(
  route: string | undefined,
  expression: string
) {
  const prefix = route ? `Route "${route}" ` : 'This route '
  return new Error(
    `${prefix}used ${expression}. ` +
      `\`cookies()\` returns a Promise and must be unwrapped with \`await\` or \`React.use()\` before accessing its properties. ` +
      `Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
  )
}
Quest for Codev2.0.0
/
SIGN IN