You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit bdf8c84
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: AGENTS.md
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -20,7 +20,7 @@ Never touch contents inside `<!-- automd -->` in README.md. They are auto genera
20
20
### Core caching (`cache.ts`)
21
21
22
22
-`defineCachedFunction(fn, opts)` β wraps any function with caching (SWR, TTL, integrity checks, deduplication of in-flight requests)
23
-
- Cache key `name` resolves as `opts.name || fn.name || \`anon_<hash(fn)>\``. Resolved once and written back into`opts`so the internal read/write path and the`resolveKeys`/`invalidate`/`expire` helpers all agree. Named/`fn.name`functions get a stable, human-readable name; anonymous functions fall back to a source hash so distinct inline fns don't collide (they would otherwise share one key and thrash). Caveat: a source hash can't tell apart same-source fns that differ only by closed-over variables β pass an explicit`name`(or`getKey`) for those. When reconstructing keys via the standalone `resolveCacheKeys`/`invalidateCache`/`expireCache`helpers (which can't see`fn`), always pass the same `name` you cached with
23
+
- Cache key `name` resolves as `opts.name || fn.name || \`anon_<hash(fn)>\``β in`_resolveName`, an internal exported from `cache.ts`and **shared with`http.ts`** (which passes the wrapped `EventHandler`as`fn`) so the two paths cannot drift; both call it on the caller's options *before* the `defaultCacheOptions()`merge, which is the whole point (see the`http.ts`bullet). Resolved once and written back into`opts`so the internal read/write path and the`resolveKeys`/`invalidate`/`expire` helpers all agree. Named/`fn.name`functions get a stable, human-readable name; anonymous functions fall back to a source hash so distinct inline fns don't collide (they would otherwise share one key and thrash). Caveat: a source hash can't tell apart same-source fns that differ only by closed-over variables β pass an explicit`name`(or`getKey`) for those. When reconstructing keys via the standalone `resolveCacheKeys`/`invalidateCache`/`expireCache`helpers (which can't see`fn`), always pass the same `name` you cached with
24
24
-`getMaxAge(entry)` option β dynamic per-entry TTL: runs after the resolver, returns a number (seconds, shorthand for `maxAge`) or `{ maxAge?, staleMaxAge? }` that override the static options for that entry. Resolved values are persisted on the entry (`CacheEntry.maxAge` / `CacheEntry.staleMaxAge`) and drive both the read freshness check and the storage TTL. Absent field / `undefined` β falls back to static options. Flows through to `defineCachedHandler`, where it runs before the internal `serialize` hook so `entry.value` is the live `Response` (inspect its headers/status; don't consume its body β `serialize` reads it exactly once)
25
25
-`serialize(entry, { args })` option β write-side counterpart of `transform`: runs once right after the resolver (and after `getMaxAge`, so that hook still sees the raw value) and returns the value to persist. `transform` deserializes it back on read. Use for resolver outputs a storage backend can't persist as-is (raw `ReadableStream`, class instances). It runs **exactly once per resolution** β shared across concurrent, deduplicated calls (every caller observes the serialized value), so consuming a one-shot source like a stream is safe. A throwing `serialize` fails the call and evicts, like a rejected resolver. Both `getMaxAge` and `serialize` are folded into the shared in-flight (`pending`) promise so they never run more than once
26
26
- The in-flight dedup registry (`pending`) is a **`Map`, never a plain object**: keys are caller-controlled (the documented `getKey: (id) => id`), and a plain object inherits from `Object.prototype`, so `pending["constructor"]` / `"toString"` / `"valueOf"` / `"hasOwnProperty"` / `"__proto__"` / `"isPrototypeOf"` / `"propertyIsEnumerable"` / `"toLocaleString"` read truthy with nothing in flight. Such a call was treated as a deduplicated follower, `await`ed the inherited member (not a thenable, so it resolved to itself), never called the resolver, and cached `undefined` β silently for `defineCachedFunction`, and as a hard, permanent `TypeError` for `defineCachedHandler` (whose `transform` then dereferences `undefined.headers`). Default keys can't hit it (the auto handler key always contains a `.`; `cache.ts`'s default is `hash(args)` or `""`), so it takes a custom `getKey`. Unrelated to prototype _pollution_ β there is none; this was unsafe prototype-chain _reading_
@@ -36,6 +36,7 @@ Never touch contents inside `<!-- automd -->` in README.md. They are auto genera
36
36
### HTTP handler caching (`http.ts`)
37
37
38
38
-`defineCachedHandler<E extends HTTPEvent>(handler, opts)` β wraps an `EventHandler` with response caching (generic over event type)
39
+
- Cache key `name`: resolved with `cache.ts`'s shared `_resolveName(opts.name, handler)` **before** the `defaultCacheOptions()` merge, exactly like `defineCachedFunction`. Merging first (the old order) left `opts.name` permanently `"_"` by the time `cachedFunction` re-derived it, so issue #53's fix never reached the HTTP layer and **every** handler keyed as `name: "_"`. Two handlers sharing one `storage` (the configuration `types.ts` recommends) that can see the same path then collided: identical source β identical integrity β one served the other's cached response (cross-handler leak); differing source β every read failed the other's integrity check β permanent thrash at 0% hit rate. **Breaking key change**, not a behavior change: every existing handler entry goes cold once (the key gains a real name segment). The `HEAD:` method component still sits directly after the name segment, so key shape is otherwise unchanged. Same **documented caveat** as `defineCachedFunction`, and it bites harder here because the shared-`storage` setup turns it into a leak: `hash(handler)` sees only source, so handlers from one factory (`(tenant) => defineCachedHandler(() => render(tenant), { storage })`) share a name _and_ an integrity β pass an explicit `name` per instance. A per-instance discriminator (counter/WeakMap/randomness) is inadmissible: keys must be deterministic across process restarts for persistent/shared backends, which is exactly why a source hash is the right fallback
39
40
- Split along the `serialize` seam (built on `cache.ts`'s `cachedFunction<Response>`): the **resolver** narrows the request and returns the handler's live `Response`; the internal **`serialize`** hook consumes the body (`res.arrayBuffer()`), synthesizes `etag`/`last-modified`/`cache-control`/`Vary`, strips every `Set-Cookie`, and builds the stored `ResponseCacheEntry`; **`transform`** reconstructs the servable shape and injects the cache-status header on read. `serialize`/`validate`/`transform` are `Omit`ted from the user-facing `CachedEventHandlerOptions` so internal use doesn't collide
40
41
- Binary responses: `serialize` reads the body as bytes and decides by **byte validity, not content-type** β a valid-UTF-8 body (fatal `TextDecoder` with `ignoreBOM`, lossless roundtrip) is stored verbatim as a string (unchanged text behavior, stable text etags); anything else (images, protobuf/MVT tiles, arbitrary binary) is base64-encoded and flagged `base64: true` on the `ResponseCacheEntry`. Base64 (not a raw `Uint8Array`) so binary bodies survive JSON-serializing storage backends. On read, a `base64` entry is decoded back to a `Uint8Array` before `createResponse`, so the exact bytes replay untouched. `createResponse` therefore receives `string | Uint8Array | null` (widened from `string | null`)
41
42
- Cache key = **resource identity + method component**. `_resolveKey` derives the method-free resource part (auto: URL path + `varies` header values + allowlisted cookies; or the caller's `getKey`), then `_methodKey` prefixes it: **GET is the implicit default and carries no component**, every other cacheable method contributes `<METHOD>:` β today exactly `HEAD:` (`_cacheableMethods`, the enumerable counterpart of `_shouldBypassCache`; making another method cacheable is a one-line addition there, not a key redesign). Closes the h3#1524 audit finding #3 poisoning: GET and HEAD used to share one entry, and a spec-compliant host `toResponse` (h3's, and every real framework's) nulls the body of a HEAD response β exactly the `Response` `serialize` stores β so one anonymous `HEAD /page` seeded the shared entry with a zero-byte body, a synthesized `public, max-age=N` and a weak etag over that empty body, and every GET for the rest of the TTL got a blank 200 that CDNs/browsers cached and successfully revalidated. Applied around **both** key branches (a custom `getKey` expresses content identity; preventing the method collision is ocache's job either way). Deliberate scoping: GET keys are byte-identical to pre-fix ones so existing entries stay warm, and the cost is one origin dispatch per method per resource per TTL. Non-cacheable methods never reach `getKey`
They still key entries by `base`/`group`/`name`, so sharing a backend does not mean sharing entries β it means sharing one connection, one eviction budget, and one place to purge.
83
83
84
+
> [!IMPORTANT]
85
+
> The `name` is what separates them, and it defaults to the function's or [handler's](/guide/handler#cache-key-name) own name, falling back to a hash of its source. Instances built by a factory or a loop have identical source, so they resolve to the **same** name β on a shared storage they then share entries. Give those an explicit `name`.
86
+
84
87
## Late Binding
85
88
86
89
Handlers are usually defined at module load, while the real backend only exists once the server has started. Pass a **factory** instead of an instance: it is called on the first actual cache read or write β never at definition time β and at most once per cached function or handler.
Copy file name to clipboardExpand all lines: docs/1.guide/5.handler.md
+36-1Lines changed: 36 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -34,7 +34,7 @@ The first request runs your handler and stores the response; subsequent requests
34
34
35
35
## What it does
36
36
37
-
-**Cache keys** β auto-generated from the request URL path (plus any query params and headers you opt into varying, see below) and the request method.
37
+
-**Cache keys** β auto-generated from the handler's [name](#cache-key-name), the request URL path (plus any query params and headers you opt into varying, see below) and the request method.
38
38
-**Conditional requests** β answers `304 Not Modified` when the client sends a matching `if-none-match` / `if-modified-since`.
39
39
-**Response headers** β synthesizes `etag`, `last-modified`, and `cache-control` when the handler didn't set them.
40
40
-**`Vary`** β advertises which request headers the response varies on (from `varies`, plus `Cookie` when `allowCookies` is set).
Every key carries a `name` component, resolved exactly like a [cached function's](/guide/functions#cache-keys): `opts.name || handler.name || anon_<hash(handler)>`. It is what keeps two handlers that share one `storage` and can see the same path from colliding.
75
+
76
+
```ts
77
+
const storage =createMemoryStorage();
78
+
79
+
// Named handlers β "/cache:handlers:dashboard:..." and "/cache:handlers:profile:..."
80
+
const dashboard =defineCachedHandler(
81
+
asyncfunction dashboard(event) {
82
+
returnrenderDashboard(event);
83
+
},
84
+
{ maxAge: 60, storage },
85
+
);
86
+
const profile =defineCachedHandler(
87
+
asyncfunction profile(event) {
88
+
returnrenderProfile(event);
89
+
},
90
+
{ maxAge: 60, storage },
91
+
);
92
+
```
93
+
94
+
> [!IMPORTANT]
95
+
> The fallback hashes the handler **source**, so handlers produced by the same factory β identical source, different closed-over variables β share a name. Sharing a `storage` then means sharing entries: one tenant's cached response is served to another. Pass an explicit `name` (or `getKey`) whenever you build handlers in a loop or a factory.
The name is derived from the handler, so a source hash is stable across restarts β which is what makes keys usable with a persistent backend.
106
+
72
107
## Varying by request headers
73
108
74
109
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:
0 commit comments