
# Cookies

**By default, cookies take no part in caching — in either direction.** For cacheable (`GET`/`HEAD`) requests:

- The `Cookie` request header is removed before the handler runs, so the handler cannot produce cookie-specific output that gets cached and served to someone else.
- Cookies never vary the cache key.
- Any `Set-Cookie` on the handler's response is removed before the response is cached **or returned**.

This keeps one visitor's cookie — a session ID, say — from ever reaching another visitor, whether through a later cache hit or through concurrent requests sharing one response.

You can opt request cookies back in per name with [`allowCookies`](#allowing-specific-cookies), or opt in the whole raw header with [`varies: ["cookie"]`](#varying-by-the-whole-cookie-header). **Response cookies have no opt-in**: `Set-Cookie` never survives on a cacheable route. See [Setting cookies](#setting-cookies).

```ts
const page = defineCachedHandler(
  async (event) => {
    event.req.headers.get("cookie"); // always null for cached GET/HEAD requests
    return new Response(await renderPage(), {
      headers: { "content-type": "text/html" },
    });
  },
  { maxAge: 60 },
);
```

> [!NOTE]
> This applies only to cacheable requests. Methods that bypass the cache — `POST`, `PUT`, and requests excluded by `shouldBypassCache` — reach the handler unchanged: cookies, headers, query, and body intact, and their `Set-Cookie` passes through untouched.

## Allowing specific cookies

`allowCookies` lists the **incoming** cookie names that participate. For those names only:

- They stay in the `Cookie` header the handler receives.
- They vary the cache key. The subset is sorted, so cookie order does not matter (as with [`allowQuery`](/docs/query-params)). Only the allowed subset is keyed — never the full raw header.

All other cookies are still removed from the request, and the response is cached as usual.

```ts
const page = defineCachedHandler(myHandler, {
  maxAge: 300,
  allowCookies: ["theme"], // theme=dark and theme=light cache separately; every other cookie is ignored
});
```

Names are **case-sensitive**. `allowCookies` overrides `varies: ["cookie"]` — if you set both, the allowlist controls the key and the filtered header the handler sees; drop the broad `varies` entry. Both forms emit the [`Vary: Cookie`](#allowcookies-emits-vary-cookie) header described below.

> [!IMPORTANT]
> Allow only cookies whose values select a shared _representation_ — a `theme` or `locale` preference. Never allow a per-user secret: every client sending the same value shares one entry.

## Varying by the whole `Cookie` header

Listing `cookie` in [`varies`](/docs/handler#varying-by-request-headers) without an allowlist is the broad form of the same opt-in — like listing `authorization` instead of setting [`allowAuthorization`](/docs/handler#credential-headers). The **raw** `Cookie` header then:

- joins the cache key as its full unmodified value,
- reaches the handler unchanged,
- is advertised as `Vary: Cookie`.

```ts
const page = defineCachedHandler(
  (event) => renderFor(event.req.headers.get("cookie")), // the full raw header
  { maxAge: 60, varies: ["cookie"] },
);
```

> [!WARNING]
> **This can mean one cache entry per visitor.** Any analytics, consent, or A/B cookie produces a distinct raw header and therefore a separate variant, and the cache can no longer combine those requests. Prefer `allowCookies` to key on the few names that select a representation; use `varies: ["cookie"]` only when the handler genuinely needs the full header.

## `allowCookies` emits `Vary: Cookie`

Setting `allowCookies` adds `Cookie` to the response [`Vary`](/docs/handler#varying-by-request-headers) header. The ocache key uses a hash of the allowed subset, but `Vary` can only name whole headers — it cannot tell a CDN that only the `theme` cookie matters.

```http
Cache-Control: max-age=300, s-maxage=300
Vary: Cookie
```

The header describes the response correctly, but it has a cost:

> [!WARNING]
> **`Vary: Cookie` collapses hit rates in CDNs and shared proxies.** Almost every visitor carries some cookie, so each distinct `Cookie` header becomes a separate downstream variant and shared caching nearly stops. Your ocache entries are unaffected — they key only on the allowed subset — but the cache _in front of_ ocache suffers.

Before ocache v1, this header was omitted: a cookie-keyed route advertised `s-maxage` without `Vary`, CDNs cached the response, and **one visitor's variant was served to everyone**. Upgrading can lower your downstream hit rate — the old rate was higher because it was serving wrong content.

If you need a high downstream hit rate, **don't key by cookie**:

- Move the choice into the URL — a `?theme=dark` parameter with [`allowQuery`](/docs/query-params), or a separate path. URLs vary the key without touching `Vary`, so CDNs can cache each variant.
- Or keep the cookie-keyed ocache entry but suppress the synthesized downstream freshness lifetime with [`sendCacheControl: false`](/docs/cache-control#suppress-synthesized-freshness-sendcachecontrol). Note this does not emit `no-store` and does not guarantee that browsers or CDNs refuse storage.

## Setting cookies

**`allowCookies` never affects the response. A `Set-Cookie` header does not survive on a cacheable (`GET`/`HEAD`) route** — it is removed from the stored entry and from the response the caller receives, including the caller whose request produced it.

There is no way to turn this off. A cached response is shared: replayed to later requests on the same key and returned to concurrent callers that shared one handler call. A cookie minted in that response would reach people it was not minted for, and an opt-in would make that leak one option away:

```ts
// Even with allowCookies: ["sid"], the Set-Cookie below never reaches any caller.
const page = defineCachedHandler(
  () => new Response("hello", { headers: { "set-cookie": `sid=${newSessionId()}` } }),
  { maxAge: 60, allowCookies: ["sid"] },
);
// First visitor:  MISS, no Set-Cookie.
// Second visitor: HIT (same key — neither had a `sid`), no Set-Cookie.
// Previously both received `sid=<the first visitor's id>` — a session-fixation hole.
```

To set a cookie, use a request that bypasses the cache. A `POST` — or any request excluded by `shouldBypassCache` — reaches the handler unchanged, and its `Set-Cookie` passes through untouched:

```ts
const login = defineCachedHandler(
  async (event) => {
    if (event.req.method === "POST") {
      // Bypassed: this Set-Cookie is neither stored nor shared.
      return new Response("ok", {
        headers: { "set-cookie": `sid=${await startSession(event)}; HttpOnly; Path=/` },
      });
    }
    return new Response(await renderLoginPage());
  },
  { maxAge: 60 },
);
```

## Caveats

- **Custom `getKey`.** As with `allowQuery`, a custom `getKey` fully controls the cache key — allowed cookies no longer vary it automatically. If the output depends on a cookie, include that cookie in your `getKey`. The `Cookie` header the handler receives is still filtered to the allowlist.
- **Entries written by older versions.** ocache refuses to serve a stored response that contains `Set-Cookie`. An older version that kept allowed cookies may have written such entries; after upgrading, they cannot be replayed from existing storage.
