Skip to content

theme: ctx.theme in a handler bakes the first visitor's theme into a shared PPR shell or cache() entry #971

Closed

Description

@ivogt

Problem

ctx.theme read in a route handler bakes the first visitor's theme into a shared PPR shell or route cache() entry. Nothing refuses or warns. cookies() in the same place is guarded: it refuses the capture, and under cache() it throws.

const router = createRouter({ theme: true }).routes([
  path("/settings", (ctx) => <p>{`theme-is-${ctx.theme}`}</p>, { ppr: true }),
]);

Served with cookie: theme=dark, then theme=light (through serveShellRequest):

miss.shellStatus MISS  entry.initialTheme dark
hit.shellStatus  HIT   hit.prelude has [ 'theme-is-dark' ]   refused warnings []

Under cache({ ttl: 300 }) with no ppr, the second visitor also gets theme-is-dark. The cookies() control is refused under ppr and returns 500 under cache().

The <html> class is still correct, because the inline theme script reads document.cookie before paint. But handler output derived from ctx.theme shows the capturing visitor's theme to everyone until the entry expires. The theme skill's own example, <SettingsPage theme={ctx.theme} /> (skills/theme/SKILL.md:93-95), hits this.

Cause

  • src/router/handler-context.ts:299 copies the value with theme: requestContext?.theme.
  • getTheme() / effectiveCookie in src/server/request-context.ts (~1140-1162) read the theme cookie without assertNotInsideCacheContext or assertNotInsideShellCapture. src/server/cookie-store.ts:64-65 applies both guards to cookies().

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

Proposed fix

  • Make the handler ctx's theme a getter that runs the same two guards as cookies(). A capture then refuses, and a cache() miss throws.
  • Keep the router's internal reqCtx.theme read unguarded (initialTheme).
  • Note in skills/theme to read the theme with useTheme() in a client component, or in a live loader, on ppr/cache() routes.
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