This release tightens security defaults (cookies and SWR), fixes cache key collision issues, and adds several new customizations (getMaxAge, shouldCache, sendCacheControl) and adds binary bodies support.
✨ New features
- Automatic
VaryandX-Cachestatus headers - Binary Response Caching
- Dynamic TTL per entry with
getMaxAge - New
serializeandshouldCachehooks - New
allowQuery: []andallowCookies: []options - Supporte async
validateandsendCacheControl: false
👀 Noticable Changes
- 🔒 Cookies are now ommited by default. Cookies are now stripped in both directions unless explicitly allowlisted (docs)
- ⏱️ SWR (stale-while-revalidate) is now opt-in, not default. Expired entries block and re-resolve in the foreground by default; add
swr: trueto keep the old "serve stale, refresh in background" behavior.
⚠️ Breaking changes
Cookies excluded from caching by default
The Cookie request header is now stripped before your handler runs, and any Set-Cookie on the response is stripped before caching — unless you allowlist the cookie name. Read docs to learn more.
swr now defaults to false
Previously, an expired cache entry was served stale by default while refreshing in the background. Now it blocks and re-resolves in the foreground by default to be more predictable. If you want the old stale-while-revalidate behavior, add swr: true explicitly (pair with staleMaxAge). If you were asserting on the synthesized Cache-Control header in tests/CDN configs, note it now emits max-age=N instead of s-maxage=N, stale-while-revalidate unless swr: true is set.
Bypassed (non-GET/HEAD) responses now pass through untouched
Internal refactor so streaming/binary bodies survive on bypassed requests, and bypassed responses no longer get synthesized cache headers or spurious 304 handling. If you relied on non-GET/HEAD responses getting cache headers or 304 handling, that no longer happens — set headers in your handler if needed for those routes.
🚀 New features
| Feature | What it's for |
|---|---|
getMaxAge hook (#39) |
Derive TTL from the resolved value itself (e.g. an OAuth token's expires_in). Runs once after the resolver; ≤0 disables caching for that entry. |
serialize hook (#59) |
Write-side counterpart to transform — serialize a raw stream/class instance before storage. Runs exactly once, shared across deduplicated concurrent calls. |
shouldCache (#48, #55) |
Extra caller-defined rule for what's cacheable (e.g. exclude redirects). ANDs with built-in checks — can only narrow, not override them. |
sendCacheControl: false (#49, #67) |
Cache server-side (storage + SWR still work) without advertising Cache-Control to clients/CDNs. Solves the "cache internally but always revalidate at the edge" case. |
| Binary response bodies (#66) | Images/protobuf/tiles now cache correctly — byte validity (not content-type) decides text vs. base64 storage. |
allowQuery (#14, #29) |
Allowlist which query params vary the cache key/reach the handler, so tracking params don't fragment your cache. |
Vary header emission (#56) |
Automatically emits Vary for your varies config so downstream CDNs store per-variant copies correctly. |
x-cache status header (#40) |
New cacheStatusHeader option (default on) reports hit/stale/revalidated/miss per response. |
Async validate (#44) |
validate can now return a Promise<boolean> — check against an external source before trusting a cached entry. |
| Bounded memory storage (#38) | Built-in memory storage now caps at 10,000 entries with LRU eviction by default (maxSize: Infinity to opt out). |
.expire()/.invalidate()/.resolveKeys() on handlers (#72) |
No more manually reconstructing cache keys to purge a defineCachedHandler route — call these directly with the event. |
🩹 Notable fixes
- Honors explicit
Cache-Control: no-store/private(#42) — these are no longer cached (and no longer silently overwritten by ocache's own synthesized header). Fixes a potential cross-user leak forprivateresponses. - Custom
getKeycollisions fixed (#60) — distinct custom keys that escaped to the same string (e.g."user:1"vs"user1:") no longer collide. One-time cold cache for anyone who hit this. shouldBypassCacheactually works now (#50, #62) — it was previously silently ignored due to an option-overwrite bug. If you built a workaround inside your handler, you can remove it.- Transport headers stripped from cached responses (#74) —
content-encoding/content-length/transfer-encodingno longer desync from the re-buffered body (was causing malformed cache-hit responses, nitro#2109). - Non-GET/HEAD bypass narrowing fixed (leftovers from #58) — bypassed requests with a body (e.g. POST) were briefly having their body dropped during the cookie/header-filtering rework; now fully untouched.
Upgrade checklist
- Audit handlers that read or set cookies → add
allowCookieswhere needed; double-check nothing sensitive gets allowlisted. - Decide on SWR per cached function/handler → add
swr: trueanywhere you want stale-serving back. - Add explicit
nameto anydefineCachedFunction/defineCachedHandlercalls that share source code but close over different variables. - Update custom
validatehooks to the(entry, { args })signature if you use call args. - Check custom
getMaxAgehooks on HTTP handlers —entry.valueis now a liveResponse; don't consume its body. - Expect a one-time cold cache for: unnamed functions (bug fix #63), colliding custom keys (#60).
❤️ Contributors
- Pooya Parsa (@pi0)
- Raminjafary (@raminjafary)
- Logosww (@Logosww)