Caching HTTP Handlers
Wrap an HTTP handler with defineCachedHandler to cache its Response automatically, complete with etag, last-modified, and 304 Not Modified support. It works with any runtime that has standard Request/Response — the event is just { req: Request; url?: URL }.
import { defineCachedHandler } from "ocache";
const handler = defineCachedHandler(
async (event) => {
const url = event.url ?? new URL(event.req.url);
const product = await db.getProduct(url.pathname.split("/").pop());
return Response.json(product);
},
{
maxAge: 300, // cache fresh for 5 minutes
swr: true, // serve stale while revalidating in the background
staleMaxAge: 600, // ...for up to 10 minutes past expiry
},
);
// Call it with any { req, url? } event
const res = await handler({ req: new Request("https://example.com/p/42") });
The first request runs your handler and stores the response; subsequent requests are served from the cache until the entry expires.
maxAge defaults to 1 second. Set it explicitly for anything you actually want cached.What it does
- Cache keys — auto-generated from the request URL path (plus any query params and headers you opt into varying, see below).
- Conditional requests — answers
304 Not Modifiedwhen the client sends a matchingif-none-match/if-modified-since. - Response headers — synthesizes
etag,last-modified, andcache-controlwhen the handler didn't set them. Vary— advertises which request headers the response varies on (fromvaries).- Binary responses — images, tiles, and other non-text bodies are cached and replayed byte-for-byte.
Cacheable requests
Only GET and HEAD requests are cached. Every other method (POST, PUT, DELETE, ...) bypasses the cache and reaches the handler untouched.
const handler = defineCachedHandler(myHandler, { maxAge: 60 });
await handler({ req: new Request(url) }); // GET → cached
await handler({ req: new Request(url, { method: "POST", body }) }); // POST → runs the handler, body intact
304.You can bypass additional requests with shouldBypassCache, which is ANDed on top of the built-in method check (it can only bypass more requests, never force-cache a non-GET/HEAD one):
const handler = defineCachedHandler(myHandler, {
maxAge: 60,
// Skip the cache for logged-in users
shouldBypassCache: (event) => event.req.headers.has("authorization"),
});
Varying by request headers
By default the cache key depends only on the URL path — every client shares one cached entry. List header names in varies to cache a separate variant per header value:
const handler = defineCachedHandler(
async (event) => {
const lang = event.req.headers.get("accept-language") ?? "en";
return new Response(renderPage(lang), {
headers: { "content-type": "text/html" },
});
},
{
maxAge: 300,
varies: ["accept-language"], // one cached entry per language
},
);
The same header names are also merged into the response Vary header (case-insensitively deduped, and a handler-set Vary: * is left untouched) so downstream caches and CDNs store per-variant too.
varies reach the handler. Every other request header is stripped before the handler runs, so a handler can never accidentally produce output that depends on a header outside the cache key. (Cookies are handled separately — see Cookies.)Conditional requests & 304
When a client re-requests with if-none-match (matching the stored etag) or if-modified-since (at or after last-modified), the handler returns 304 Not Modified with an empty body:
const handler = defineCachedHandler(() => new Response("<h1>Hi</h1>"), {
maxAge: 300,
});
const first = await handler({ req: new Request(url) });
const etag = first.headers.get("etag");
// Browser revalidates with the etag it received
const second = await handler({
req: new Request(url, { headers: { "if-none-match": etag } }),
});
second.status; // 304
The 304 echoes the Vary header so downstream caches keep the variant dimension.
Headers-only mode
Set headersOnly: true to answer conditional requests without caching the response body. On a conditional match the handler short-circuits with a 304; otherwise your handler runs and its response is returned as-is (never stored):
const handler = defineCachedHandler(myHandler, {
headersOnly: true,
maxAge: 60,
});
Use this when the handler already produces its own etag/last-modified and you only want the 304 shortcut, not full response storage.
Cache-status header
By default a CDN-style X-Cache header is added, reporting how the response was served: MISS, HIT, STALE, or REVALIDATED.
defineCachedHandler(myHandler, { maxAge: 60 }); // adds X-Cache
defineCachedHandler(myHandler, { maxAge: 60, cacheStatusHeader: "x-my-cache" }); // custom name
defineCachedHandler(myHandler, { maxAge: 60, cacheStatusHeader: false }); // disable
Framework integration hooks
To adapt to a specific framework's request/response types, override any of these:
toResponse(value, event)— convert the handler's return value into aResponse. Default: return it as-is if it's already aResponse, otherwisenew Response(String(value)).createResponse(body, init)— build the finalResponseserved from the cache.bodyis astring(text), aUint8Array(binary), ornull(empty /304). Default:new Response(body, init).handleCacheHeaders(event, conditions)— decide whether to answer with a304. Returntrueto short-circuit. Default: the built-inif-none-match/if-modified-sincecheck.
const handler = defineCachedHandler(
// Handler returns a plain object...
async (event) => ({ id: 42, name: "Widget" }),
{
maxAge: 300,
// ...toResponse turns it into JSON
toResponse: (value) => Response.json(value),
},
);
On-demand revalidation
The returned handler carries .expire(event), .invalidate(event), and .resolveKeys(event), mirroring the methods on a cached function. Each takes the request event and derives the exact key the handler cached it under:
await page.expire(event); // mark stale — serve stale once more, refresh in background (SWR)
await page.invalidate(event); // remove — next request blocks on a fresh render
See ISR → on-demand revalidation for the full recipe.
More
- Query Parameters — control which params vary the cache.
- Cookies — the secure default and how to opt cookies back in.
- Cache-Control & Eligibility —
no-store/private,sendCacheControl,shouldCache. - Incremental Static Regeneration (ISR) — serve stale, revalidate in the background.