
# Migrating to v0.3

ocache [v0.3.0](https://github.com/unjs/ocache/releases/tag/v0.3.0) is now available. This guide covers upgrading from v0.2. Most applications can upgrade by choosing a storage instance, clearing persistent entries, and auditing what cached HTTP handlers read and return. The [v0.1 to v0.2 summary](#coming-from-v01) covers applications upgrading across both releases.

## Upgrade checklist

1. Replace `setStorage()` / `useStorage()` with the `storage` option.
2. Flush persistent v0.2 entries or move v0.3 to a new `base` prefix.
3. Declare every request header a cached handler reads in `varies` (or use the credential and cookie options), and every query parameter it reads in `allowQuery`.
4. Split cookie-setting responses out of cacheable routes.
5. Check that cached responses use a cacheable status and do not opt out through `Cache-Control` or `Vary`.
6. Set `maxResolveTime` and memory/body limits explicitly if the new defaults do not fit the workload.

## Storage is per cache instance

v0.2 used one process-global backend through `setStorage()` and `useStorage()`. v0.3 removes both exports. Every cached function and handler now owns a backend; its default is a fresh memory store that is not shared with any other instance.

Before:

```ts
import { defineCachedFunction, setStorage } from "ocache";

setStorage(storage);

const getUser = defineCachedFunction(fetchUser, { maxAge: 60 });
const getPosts = defineCachedFunction(fetchPosts, { maxAge: 60 });
```

After:

```ts
import { defineCachedFunction } from "ocache";

const getUser = defineCachedFunction(fetchUser, { maxAge: 60, storage });
const getPosts = defineCachedFunction(fetchPosts, { maxAge: 60, storage });
```

Pass the **same instance** when caches should share a connection, eviction budget, or persistent namespace. A factory can defer backend creation until the first operation:

```ts
const handler = defineCachedHandler(renderPage, {
  storage: () => storage,
});
```

The factory runs once per cached function or handler. See [Storage](/docs/storage#late-binding).

### Standalone invalidation needs storage

`invalidateCache()` and `expireCache()` no longer fall back to a global backend. They throw when `options.storage` is absent. Prefer the methods attached to a cached function or handler:

```ts
await getUser.invalidate(id);
await handler.expire(event);
```

When a standalone helper is necessary, pass the original options object or reconstruct the options with the same backend:

```ts
const cacheOptions = {
  name: "getUser",
  getKey: (id: string) => id,
  maxAge: 60,
  storage,
};

const getUser = defineCachedFunction(fetchUser, cacheOptions);

await invalidateCache({ options: cacheOptions, args: [id] });
```

The instance methods also prevent an in-flight resolution or delayed write from undoing a purge. Standalone helpers cannot coordinate with an instance, so use them only when no instance reference is available. See [Invalidation & Expiration](/docs/invalidation#standalone-helpers).

## Start with an empty persistent namespace

Do not expect v0.2 and v0.3 to share persistent entries. Several corrections intentionally change cache identity:

- handler keys include the request scheme, host, and port;
- `HEAD` has a separate key from `GET`;
- every `name` and `group` segment is escaped so it cannot alter key structure;
- custom handler keys are escaped for the same reason;
- argument, integrity, path, header, cookie, and ETag hashes use the standalone deterministic serializer and SHA-256 implementation;
- malformed or unsafe entries are rejected more strictly on read.

Flush the old namespace during deployment, or change `base` while rolling out:

```ts
const options = {
  base: "/cache-v3",
  storage,
};
```

Using a new prefix is the safest rolling-upgrade strategy because old and new processes can otherwise read or overwrite entries under incompatible key and validation rules. Remove the v0.2 prefix after all old processes and entries are gone.

> [!IMPORTANT]
> Set explicit, stable `name` values for functions or handlers created in factories and loops. Equal-source closures receive the same generated name and therefore share entries when they use the same storage.

## Cached handlers see only keyed request data

A cached handler may now read exactly the inputs represented by its key. On cacheable requests, ocache removes every undeclared request header before calling the handler. Add each representation-changing header to `varies`:

```ts
const handler = defineCachedHandler(renderPage, {
  varies: ["accept-language", "user-agent"],
});
```

This is an allowlist. Audit reads of `event.req.headers`, including reads hidden in utilities called by the handler. There are no implicit exemptions for conditional, tracing, or request-ID headers. If output depends on one, declare it; if it is needed only for logging or tracing, capture it in middleware or elsewhere on the framework event before the cached handler runs.

Other request inputs follow the same rule:

- `allowQuery` controls both the query names in the generated key and the query names visible to the handler. It is now **opt-in**: with the option unset, no query parameter varies the key and the handler receives a query-less URL. `allowQuery: true` restores the v0.2 behavior of keying and forwarding the full query string.
- `allowCookies` controls both the cookie subset in the key and the filtered `Cookie` header visible to the handler.
- `varies: ["cookie"]` opts in the complete raw `Cookie` header.
- `allowAuthorization: true` opts in `Authorization` and `Proxy-Authorization`; by default both are removed.
- a custom `getKey` replaces generated identity, but does **not** disable request filtering.

If a request must reach the handler unchanged, bypass caching instead:

```ts
const handler = defineCachedHandler(renderPrivatePage, {
  shouldBypassCache: (event) => event.req.headers.has("authorization"),
});
```

Non-cacheable methods and requests with `Range` also bypass without narrowing or response rewriting.

### Host and multi-host applications

Generated handler keys now include the resolved URL authority. The handler's `Host` header is rewritten to the host from that URL, ensuring the visible value is covered by the key. Applications that serve more than one origin no longer share entries for equal paths.

Check adapters that supply both `event.url` and `event.req.url`: they must describe the intended public authority. If output intentionally depends on a raw forwarded host header, list that header in `varies` rather than relying on an undeclared value.

## Cookie responses no longer pass through

A cacheable response can be replayed to later callers and shared by concurrent callers, so v0.3 always removes `Set-Cookie`. `allowCookies` controls request cookies only; it does not opt response cookies back in.

Move cookie creation to a non-cacheable request or bypass that request:

```ts
const handler = defineCachedHandler(route, {
  shouldBypassCache: (event) => event.req.url.endsWith("/session"),
});
```

`allowCookies` and `varies: ["cookie"]` now emit `Vary: Cookie`. This may reduce browser or CDN hit rates, but omitting it can serve one cookie-selected representation to callers with another cookie value. For server-only cookie variation, consider `sendCacheControl: false`; note that this suppresses only ocache's generated freshness header and is not an explicit downstream `no-store` policy.

See [Cookies](/docs/cookies) for safe patterns.

## HTTP cache eligibility is stricter

v0.2 broadly rejected errors and accepted many other responses. v0.3 stores only responses that pass all built-in checks.

### Status allowlist

Only these statuses are stored:

- `200 OK`
- `203 Non-Authoritative Information`
- `301 Moved Permanently`
- `308 Permanent Redirect`

Responses with null bodies, including `204`, `205`, and `304`, are not stored. Use `shouldCache` to narrow this list further; it cannot widen it.

### Response opt-outs

A response is returned live but not stored when it has any of these properties:

- `Cache-Control: no-store`, `private`, or `no-cache`;
- an effective `s-maxage=0` or `max-age=0`;
- `Vary: *`;
- a `Vary` name not covered by `varies`, `allowCookies`, or `allowAuthorization`;
- a non-positive static or dynamic `maxAge`;
- a body larger than `maxBodySize`.

A response rejected because of its status or `Vary` value receives no synthesized `Cache-Control`. Handler-provided headers remain intact except for `Set-Cookie` on cacheable routes. An oversized response bypasses serialization entirely, so it receives none of ocache's synthesized headers.

If a handler declares `Vary`, add every listed name to the cache configuration. Do not use `Vary: *` on a response intended for storage.

### Cache-Control follows the enforced lifetime

Generated `Cache-Control` now describes the entry's actual static or `getMaxAge` lifetime. In particular:

- dynamic `maxAge` and `staleMaxAge` values are advertised;
- `s-maxage` takes precedence over `max-age` when parsing handler directives;
- `must-revalidate` prevents stale serving for that response;
- unlimited SWR does not emit a valueless `stale-while-revalidate` directive;
- `sendCacheControl: false` suppresses only the generated header.

Review CDN behavior if it depended on v0.2's static or valueless directives. See [Cache-Control & Eligibility](/docs/cache-control).

## Conditional requests and validators

v0.3 continues to synthesize an ETag for stored bodies, but no longer synthesizes `Last-Modified`. A response participates in date validation only when the handler provides that header.

ETag values for binary responses change because text and binary representations now use separate hash domains. Clear persistent entries as described above rather than mixing validator formats.

Conditional matching is also more complete:

- `If-None-Match` takes precedence over `If-Modified-Since`;
- weak tags, tag lists, and `*` are supported;
- a `304` echoes the stored ETag, Last-Modified, Cache-Control, Expires, Vary, and cache-status policy where present;
- `headersOnly: true` always runs the handler and compares the request against that live response's validators.

Custom `handleCacheHeaders` hooks should read the captured `conditions.ifNoneMatch` and `conditions.ifModifiedSince`. The narrowed `event.req` intentionally no longer contains those undeclared headers.

## New resource limits

### Resolution deadline

A shared resolution and its hooks now have a **30-second default deadline**. On timeout, all waiters reject, the old entry is evicted, and the key becomes available for another resolution.

Disable the deadline or choose a workload-specific value explicitly:

```ts
const cached = defineCachedFunction(slowJob, {
  maxResolveTime: 120,
  // maxResolveTime: 0, // disable
});
```

For cached handlers, the leading request receives the deadline through `event.req.signal`. Pass it to abort-aware upstream work:

```ts
const handler = defineCachedHandler(async (event) => {
  return fetch(upstream, { signal: event.req.signal });
});
```

### Memory and response bodies

The built-in memory backend still defaults to 10,000 entries, but now also defaults to a **100 MB estimated byte budget**. It evicts least-recently-used entries when either ceiling is exceeded. New options are available:

```ts
const storage = createMemoryStorage({
  maxSize: 20_000,
  maxBytes: 256 * 1024 * 1024,
  sizeOf: (value, key) => measure(value, key),
});
```

An entry larger than `maxBytes`, or one whose size cannot be measured, is refused. Set `Infinity` or `0` to disable a limit.

Cached handlers also stop buffering bodies larger than `maxBodySize`. Its default is derived from the backend's optional `maxEntryBytes` declaration; a backend without a declared ceiling has no derived body limit. An oversized response is served live and complete, but is not stored or decorated with generated cache headers.

Backends that preserve `Uint8Array` values may declare `binary: true` to store binary response bodies without base64. Do not set it on JSON-serializing backends. See [the storage interface](/docs/storage#the-storage-interface) and [Response size](/docs/handler#response-size).

## Coming from v0.1

Upgrade v0.1 applications to the v0.2 behavior below first, then apply the v0.2-to-v0.3 changes above.

### SWR became opt-in

v0.1 enabled stale-while-revalidate by default. Since v0.2, `swr` defaults to `false`: an expired entry blocks while it is revalidated. Add `swr: true` anywhere that must continue serving stale data in the background.

`staleMaxAge: 0` now means no stale serving. Use a positive number for a bounded stale window, or omit it with `swr: true` for a window bounded only by backend eviction.

### `validate` changed signature

The arguments moved into a context object and the hook may be asynchronous.

Before v0.2:

```ts
validate: (entry, id, locale) => entry.value.id === id,
```

Since v0.2:

```ts
validate: async (entry, { args: [id, locale] }) => entry.value.id === id,
```

Validation receives the stored shape when `serialize` is configured.

### Generated function names changed keys

When `name` is omitted, v0.2 uses `fn.name`, with a source hash fallback for anonymous functions, instead of placing every function under `"_"`. This fixes collisions but changes keys. Flush old persistent entries or keep an explicit legacy `name` during a staged migration. Factory-created equal-source closures still need distinct explicit names when they share storage.

### Cookies became deny-by-default

v0.1 handlers could observe cookies without representing them safely in the key. v0.2 removes request cookies on cacheable routes unless they are explicitly selected:

```ts
const handler = defineCachedHandler(render, {
  allowCookies: ["theme", "locale"],
});
```

`allowCookies` filters the `Cookie` header and keys the allowed subset. `varies: ["cookie"]` selects the whole raw header. In v0.2, non-allowlisted `Set-Cookie` values were removed while allowed ones could survive; v0.3 tightens this further and removes every `Set-Cookie` from cacheable responses.

### Bypassed responses pass through unchanged

Since v0.2, non-cacheable methods and `shouldBypassCache` responses skip serialization and transformation. Streaming and binary bodies, request credentials, full query strings, and handler-provided headers pass through untouched. Do not rely on cache-generated ETag, Cache-Control, or status headers on a bypass path.

### APIs added across v0.1.x and v0.2

The v0.1.x releases added multi-tier `base` prefixes plus `.resolveKeys()`, `.invalidate()`, and `.expire()` on cached functions. v0.2 extended the same management methods to cached handlers and added APIs that replace common application-side workarounds:

- `getMaxAge` for per-entry lifetimes;
- `serialize` for converting values once before storage;
- async `validate` and `shouldCache` for narrowing eligibility;
- `allowQuery` for key-and-request query filtering;
- `cacheStatusHeader` and `CacheEntry.status` for hit/miss reporting;
- `sendCacheControl: false` for suppressing synthesized downstream freshness;
- binary HTTP response preservation;
- `.resolveKeys()`, `.invalidate()`, and `.expire()` on cached handlers;
- `createMemoryStorage({ maxSize })` with a 10,000-entry LRU default.

The v0.3 sections above supersede v0.2 where behavior changed again, especially storage ownership, cookie responses, byte limits, key construction, and HTTP eligibility.

## Smaller v0.3 behavior changes

- Returning `false`, `0`, or `""` from `transform` now returns that value instead of falling back to the cached value.
- Explicit `undefined` option values now fall back to defaults instead of overwriting them. `null` remains an explicit value where the option accepts it.
- `.invalidate()` and `.expire()` cover all `base` tiers; handler methods cover both `GET` and `HEAD` variants of the resource.
- Stale values are served only when their integrity matches the current function and options.
- Non-positive `maxAge` values are not written as immortal, unusable entries.

After migrating, run representative requests twice and verify `X-Cache: MISS` followed by `X-Cache: HIT`, then test bypass, conditional, cookie, authorization, oversized-response, and invalidation paths relevant to the application.
