Skip to content

cache: a loader bound with its own cache() that reads cookies() serves the first visitor's value to everyone #972

Closed

Description

@ivogt

Problem

A loader bound with its own cache() that reads cookies() stores the first visitor's cookie-derived value in the shared loader entry and serves it to everyone for the TTL. Nothing throws or warns.

const SessionLoader = createLoader(async () => ({ session: cookies().get("session")?.value }));
// loader(SessionLoader, () => [cache({ ttl: 60 })])
runLoader  userA {"session":"a"}  userB {"session":"a"}  stored [ 'loader:...:localhost/account' ]  warnings []
router     userA [ 'session-a' ]  userB [ 'session-a' ]   loader body runs 1
keyed control (cache.key reads the cookie): userB {"session":"b"}

The docs point consumers toward this:

  • skills/caching/SKILL.md:91-93: cookies() "belongs in a loader (loaders always run fresh)".
  • skills/cache-guide/SKILL.md:113: "Loaders are exempt".
  • skills/theme/SKILL.md:108-116.
  • skills/loader/SKILL.md:630-658 documents the default key without saying that a body reading cookies() needs a key().

Cause

  • src/server/context.ts:884: isInsideCacheScope() returns false inside any loader body, so cookies() doesn't throw.
  • src/router/segment-resolution/loader-cache.ts:112-141 keys the entry by loader id, host, path and params only.

The same output reproduces on main f24d459 and on v0.17.0.

Proposed fix

Guard the loader-cache MISS body the way "use cache" bodies are guarded, when the binding has no key() and the store has no keyGenerator: cookies(), headers() and non-cacheable ctx.get throw (or warn once; to decide). Document in /loader "Cache Key" that request-scoped reads in the body must be part of key(). Fix the "loaders always run fresh" wording in the skills above.

Activity

  1. ivogt commented on Sep 29, 2026

    @ivogt
    CollaboratorAuthor

    Decision: throw, not warn. The data is user/session-scoped. A cached loader body with no key() (and no store keyGenerator) that reads cookies(), headers() or a non-cacheable variable throws, like "use cache" and handlers under cache(). Adding a key() that includes the value is the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions