Skip to content

Commit

Permalink
feat(cache): support cached event handlers with varies (#1184)
Browse files Browse the repository at this point in the history
Co-authored-by: Pooya Parsa <pooya@pi0.io>
  • Loading branch information
Botz and pi0 authored Aug 7, 2023
1 parent 1b9815f commit 9ce2f64
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 14 deletions.
6 changes: 6 additions & 0 deletions docs/content/1.guide/5.cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ The response will be cached for 10 second and a stale value will be sent to the

The cached answer will be store in development inside `.nitro/cache/handlers/_/*.json`.

::alert{type=primary}
By default, all incoming request headers are dropped when handling cached responses. If you define the `varies` option, only the specified headers will be considered when caching and serving the responses.
::

### Function

Cache for 1 hour the result of a function fetching the GitHub stars for a repository:
Expand Down Expand Up @@ -221,3 +225,5 @@ The `cachedEventHandler` and `cachedFunction` functions accept the following opt
- Type: `Function`
- `shouldBypassCache`: A function that returns a boolean to bypass the current cache without invalidating the existing entry.
- Type: `Function`
- `varies`: An array of request headers to be considered for the cache
- Type: `string[]`
44 changes: 30 additions & 14 deletions src/runtime/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,31 +182,39 @@ export interface CachedEventHandlerOptions<T = any>
shouldBypassCache?: (event: H3Event) => boolean;
getKey?: (event: H3Event) => string | Promise<string>;
headersOnly?: boolean;
varies?: string[];
}

function escapeKey(key: string) {
// eslint-disable-next-line unicorn/prefer-string-replace-all
return key.replace(/[^\dA-Za-z]/g, "");
function escapeKey(key: string | string[]) {
return String(key).replace(/\W/g, "");
}

export function defineCachedEventHandler<T = any>(
handler: EventHandler<T>,
opts: CachedEventHandlerOptions<T> = defaultCacheOptions
): EventHandler<T> {
const variableHeaderNames = (opts.varies || [])
.filter(Boolean)
.map((h) => h.toLowerCase())
.sort();

const _opts: CacheOptions<ResponseCacheEntry<T>> = {
...opts,
getKey: async (event: H3Event) => {
const key = await opts.getKey?.(event);
if (key) {
return escapeKey(key);
// Custom user-defined key
const customKey = await opts.getKey?.(event);
if (customKey) {
return escapeKey(customKey);
}
const url = event._originalPath || event.path;
const friendlyName = escapeKey(decodeURI(parseURL(url).pathname)).slice(
0,
16
);
const urlHash = hash(url);
return `${friendlyName}.${urlHash}`;
// Auto-generated key
const _path = event._originalPath || event.path;
const _pathname =
escapeKey(decodeURI(parseURL(_path).pathname)).slice(0, 16) || "index";
const _hashedPath = `${_pathname}.${hash(_path)}`;
const _headers = variableHeaderNames
.map((header) => [header, event.node.req.headers[header]])
.map(([name, value]) => `${escapeKey(name)}.${hash(value)}`);
return [_hashedPath, ..._headers].join(":");
},
validate: (entry) => {
if (entry.value.code >= 400) {
Expand All @@ -223,8 +231,16 @@ export function defineCachedEventHandler<T = any>(

const _cachedHandler = cachedFunction<ResponseCacheEntry<T>>(
async (incomingEvent: H3Event) => {
// Only pass headers which are defined in opts.varies
const variableHeaders: Record<string, string | string[]> = {};
for (const header of variableHeaderNames) {
variableHeaders[header] = incomingEvent.node.req.headers[header];
}

// Create proxies to avoid sharing state with user request
const reqProxy = cloneWithProxy(incomingEvent.node.req, { headers: {} });
const reqProxy = cloneWithProxy(incomingEvent.node.req, {
headers: variableHeaders,
});
const resHeaders: Record<string, number | string | string[]> = {};
let _resSendBody;
const resProxy = cloneWithProxy(incomingEvent.node.res, {
Expand Down

0 comments on commit 9ce2f64

Please sign in to comment.