HTTP Cache Semantics Policy
This library provides a robust implementation of HTTP caching logic as defined by RFC 7234/9111 (for user agents and shared caches) and RFC 5861 (for `stale-if-error` and `stale-while-revalidate`). It allows developers to determine the cacheability of HTTP responses and whether a stored response can satisfy a new request, taking into account complex factors like the `Vary` header, proxy revalidation, and authenticated responses. The current stable version is 4.2.0. The library helps in building correct HTTP caches and proxies by abstracting away the intricacies of cache control directives, providing methods like `storable()` to check if a response can be cached, and `satisfiesWithoutRevalidation()` to validate if a cached entry is suitable for a subsequent request. It offers fine-grained control via constructor options like `shared`, `cacheHeuristic`, and `immutableMinTimeToLive`.
Common errors
-
TypeError: Cannot read properties of undefined (reading 'headers')
cause The `request` or `response` object passed to the `CachePolicy` constructor is missing the `headers` property, or it is `null`/`undefined`.fixEnsure both `request` and `response` objects always include a `headers` property, even if it's an empty object (`{}`), before being used to instantiate `CachePolicy`. -
Cached response not served, even though 'Cache-Control: max-age' indicates it should be fresh.
cause The cached response has a `Vary` header, and the headers of the new request do not match those of the original request used to create the cache entry, or the new request contains restrictive cache control directives (e.g., `Cache-Control: no-cache`).fixAlways use `policy.satisfiesWithoutRevalidation(newRequest)` to check if a cached response can be validly served for a new request. This method correctly evaluates `Vary` headers and other request-specific conditions, which simple freshness checks like `timeToLive()` do not. -
Incorrect caching of `private` or `s-maxage` responses in a shared/single-user context.
cause The `shared` option in the `CachePolicy` constructor is not correctly configured for the caching environment (e.g., `shared: false` for a proxy, or `shared: true` for a single-user cache leading to private data exposure).fixSet `options.shared: true` (the default) for shared caches (e.g., HTTP proxies) to respect `s-maxage` and treat `private` as non-cacheable. Set `options.shared: false` for single-user caches (e.g., browser-like caches) to ignore `s-maxage` and allow caching of `private` responses.
Warnings
- gotcha All request and response header names passed to `CachePolicy` must be provided in lowercase. Failing to do so will result in incorrect caching behavior, as the library expects standardized header processing.
- gotcha Misunderstanding the `shared` option can lead to incorrect cacheability decisions. If `options.shared` is `true` (the default), `private` responses are not cacheable, and `s-maxage` takes precedence. If `false`, then `private` is cacheable, and `s-maxage` is ignored. This is critical for distinguishing between shared proxies and single-user caches.
- gotcha The `storable()` method only indicates if a response *can* be stored in the cache. To determine if a *cached* response can satisfy a *new* request, you *must* use `satisfiesWithoutRevalidation(newRequest)`. This method accounts for `Vary` headers, request-specific cache directives, and other critical HTTP rules.
- gotcha Improper handling of the `Vary` header is a common cause of incorrect caching. `http-cache-semantics` automatically considers `Vary` in `satisfiesWithoutRevalidation()`, but developers must be aware that a cached response might not be valid if the `Vary` header fields on the new request do not match the original cached request's headers.
- gotcha Using `ignoreCargoCult: true` can override common anti-cache directives (like `pre-check` and `post-check`) if non-standard directives are present. While useful for problematic origins, indiscriminately enabling this option might lead to over-caching of responses intended to be fresh.
Install
-
npm install http-cache-semantics -
yarn add http-cache-semantics -
pnpm add http-cache-semantics
Imports
- CachePolicy
const CachePolicy = require('http-cache-semantics').CachePolicy;import { CachePolicy } from 'http-cache-semantics'; - CachePolicyOptions
import type { CachePolicyOptions } from 'http-cache-semantics'; - CachePolicy
import CachePolicy from 'http-cache-semantics';
import { CachePolicy } from 'http-cache-semantics';
Quickstart
import { CachePolicy } from 'http-cache-semantics';
// Simulate an initial request and its corresponding response
const initialRequest = {
url: 'https://api.example.com/data/items',
method: 'GET',
headers: {
'accept': 'application/json',
'authorization': 'Bearer mysecrettoken'
},
};
const initialResponse = {
status: 200,
headers: {
'cache-control': 'public, max-age=3600, s-maxage=600',
'content-type': 'application/json',
'vary': 'Accept, Authorization', // Note: Library expects lowercase, but here we simulate a common server response
'date': new Date().toUTCString(),
},
body: '{"id": 1, "name": "Cached Item"}',
};
// Options for the cache policy evaluation
const options = {
shared: true, // Evaluate from a shared cache perspective (e.g., a proxy)
cacheHeuristic: 0.1, // 10% of response's age as fallback TTL
immutableMinTimeToLive: 24 * 3600 * 1000, // 24 hours for 'immutable'
};
// --- Step 1: Create and store the cache policy for an incoming response ---
// Ensure headers are lowercase before passing to CachePolicy
const lowercaseInitialRequest = { ...initialRequest, headers: Object.fromEntries(Object.entries(initialRequest.headers).map(([k, v]) => [k.toLowerCase(), v])) };
const lowercaseInitialResponse = { ...initialResponse, headers: Object.fromEntries(Object.entries(initialResponse.headers).map(([k, v]) => [k.toLowerCase(), v])) };
const policy = new CachePolicy(lowercaseInitialRequest, lowercaseInitialResponse, options);
if (!policy.storable()) {
console.log("Initial response is not storable in the cache. Discarding.");
} else {
const letsPretendThisIsSomeCache = new Map<string, { policy: CachePolicy, body: string }>();
const timeToLive = policy.timeToLive();
console.log(`Response is storable. Estimated time to live: ${timeToLive}ms`);
// Store the policy object along with the response body
letsPretendThisIsSomeCache.set(initialRequest.url, { policy, body: initialResponse.body });
// --- Step 2: Later, an identical new request comes in ---
const newRequest = {
url: 'https://api.example.com/data/items',
method: 'GET',
headers: {
'accept': 'application/json',
'authorization': 'Bearer mysecrettoken'
},
};
const cachedEntry = letsPretendThisIsSomeCache.get(newRequest.url);
if (cachedEntry) {
// Ensure headers are lowercase for the new request too
const lowercaseNewRequest = { ...newRequest, headers: Object.fromEntries(Object.entries(newRequest.headers).map(([k, v]) => [k.toLowerCase(), v])) };
if (cachedEntry.policy.satisfiesWithoutRevalidation(lowercaseNewRequest)) {
// The cached response is valid for the new request without revalidation
const responseHeaders = cachedEntry.policy.responseHeaders();
console.log("Cached response can be used without revalidation.");
console.log("Updated response headers for client (includes Age, removes private headers):", responseHeaders);
// In a real application, you would send { headers: responseHeaders, body: cachedEntry.body } to the client.
} else {
console.log("Cache hit, but revalidation is required or response is not suitable for this new request.");
// Implement revalidation logic using policy.revalidationHeaders() and policy.revalidatedPolicy()
}
} else {
console.log("Cache miss. No entry found for this URL.");
// Fetch fresh data
}
}