Skip to content

Commit 82d1d41

Browse files
committed
Simplify evaluateRequest API
1 parent 1e6e9ad commit 82d1d41

File tree

2 files changed

+150
-68
lines changed

2 files changed

+150
-68
lines changed

README.md

Lines changed: 100 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Can I cache this? [![Build Status](https://travis-ci.org/kornelski/http-cache-semantics.svg?branch=master)](https://travis-ci.org/kornelski/http-cache-semantics)
1+
# Can I cache this?
22

33
This library tells when responses can be reused from a cache, taking into account [HTTP RFC 7234/9111](http://httpwg.org/specs/rfc9111.html) rules for user agents and shared caches.
44
It also implements `stale-if-error` and `stale-while-revalidate` from [RFC 5861](https://tools.ietf.org/html/rfc5861).
@@ -90,7 +90,7 @@ Returns `true` if the response can be stored in a cache. If it's `false` then yo
9090

9191
### `satisfiesWithoutRevalidation(newRequest)`
9292

93-
This is the most important method. Use this method to check whether the cached response is still fresh in the context of the new request.
93+
Use this method to check whether the cached response is still fresh in the context of the new request.
9494

9595
If it returns `true`, then the given `request` matches the original response this cache policy has been created with, and the response can be reused without contacting the server. Note that the old response can't be returned without being updated, see `responseHeaders()`.
9696

@@ -112,7 +112,98 @@ After that time (when `timeToLive() <= 0`) the response may still be usable in c
112112

113113
### `toObject()`/`fromObject(json)`
114114

115-
Chances are you'll want to store the `CachePolicy` object along with the cached response. `obj = policy.toObject()` gives a plain JSON-serializable object. `policy = CachePolicy.fromObject(obj)` creates an instance from it.
115+
You'll want to store the `CachePolicy` object along with the cached response. `obj = policy.toObject()` gives a plain JSON-serializable object. `policy = CachePolicy.fromObject(obj)` creates an instance from it.
116+
117+
## Complete Usage
118+
119+
### `evaluateRequest(newRequest)`
120+
121+
Returns an object telling what to do next — optional `revalidation`, and optional `response` from cache. Either one of these properties will be present. Both may be present at the same time.
122+
123+
```js
124+
{
125+
// If defined, you must send a request to the server.
126+
revalidation: {
127+
headers: {}, // HTTP headers to use when sending the revalidation response
128+
// If true, you MUST wait for a response from the server before using the cache
129+
// If false, this is stale-while-revalidate. The cache is stale, but you can use it while you update it asynchronously.
130+
synchronous: bool,
131+
},
132+
// If defined, you can use this cached response.
133+
response: {
134+
headers: {}, // Updated cached HTTP headers you must use when responding to the client
135+
},
136+
}
137+
```
138+
139+
### Example
140+
141+
```js
142+
let cached = cacheStorage.get(incomingRequest.url);
143+
144+
// Cache miss - make a request to the origin and cache it
145+
if (!cached) {
146+
const newResponse = await makeRequest(incomingRequest);
147+
const policy = new CachePolicy(incomingRequest, newResponse);
148+
149+
cacheStorage.set(
150+
incomingRequest.url,
151+
{ policy, body: newResponse.body },
152+
policy.timeToLive()
153+
);
154+
155+
return {
156+
// use responseHeaders() to remove hop-by-hop headers that should not be passed through proxies
157+
headers: policy.responseHeaders(),
158+
body: newResponse.body,
159+
}
160+
}
161+
162+
// There's something cached, see if it's a hit
163+
let { revalidation, response } = cached.policy.evaluateRequest(incomingRequest);
164+
165+
// Revalidation always goes first
166+
if (revalidation) {
167+
// It's very important to update the request headers to make a correct revalidation request
168+
incomingRequest.headers = revalidation.headers; // Same as cached.policy.revalidationHeaders()
169+
170+
// The cache may be updated immediately or in the background,
171+
// so use a Promise to optionally defer the update
172+
const updatedResponsePromise = makeRequest(incomingRequest).then(() => {
173+
// Refresh the old response with the new information, if applicable
174+
const { policy, modified } = cached.policy.revalidatedPolicy(incomingRequest, newResponse);
175+
176+
const body = modified ? newResponse.body : cached.body;
177+
178+
// Update the cache with the newer response
179+
if (policy.storable()) {
180+
cacheStorage.set(
181+
incomingRequest.url,
182+
{ policy, body },
183+
policy.timeToLive()
184+
);
185+
}
186+
187+
return {
188+
headers: policy.responseHeaders(), // these are from the new revalidated policy
189+
body,
190+
}
191+
});
192+
193+
if (revalidation.synchronous) {
194+
// If synchronous, then you MUST get a reply from the server first
195+
return await updatedResponsePromise;
196+
}
197+
198+
// If not synchronous, it can fall thru to returning the cached response,
199+
// while the request to the server is happening in the background.
200+
}
201+
202+
return {
203+
headers: response.headers, // Same as cached.policy.responseHeaders()
204+
body: cached.body,
205+
}
206+
```
116207

117208
### Refreshing stale cache (revalidation)
118209

@@ -124,7 +215,7 @@ The following methods help perform the update efficiently and correctly.
124215

125216
Returns updated, filtered set of request headers to send to the origin server to check if the cached response can be reused. These headers allow the origin server to return status 304 indicating the response is still fresh. All headers unrelated to caching are passed through as-is.
126217

127-
Use this method when updating cache from the origin server.
218+
Use this method when updating cache from the origin server. Also available in `evaluateRequest(newRequest).revalidation.headers`.
128219

129220
```js
130221
updateRequest.headers = cachePolicy.revalidationHeaders(updateRequest);
@@ -135,45 +226,9 @@ updateRequest.headers = cachePolicy.revalidationHeaders(updateRequest);
135226
Use this method to update the cache after receiving a new response from the origin server. It returns an object with two keys:
136227

137228
- `policy` — A new `CachePolicy` with HTTP headers updated from `revalidationResponse`. You can always replace the old cached `CachePolicy` with the new one.
138-
- `modified` — Boolean indicating whether the response body has changed.
139-
- If `false`, then a valid 304 Not Modified response has been received, and you can reuse the old cached response body. This is also affected by `stale-if-error`.
140-
- If `true`, you should use new response's body (if present), or make another request to the origin server without any conditional headers (i.e. don't use `revalidationHeaders()` this time) to get the new resource.
141-
- `headers` — updated response headers to use when returning the response to the client.
142-
143-
```js
144-
// When serving requests from cache:
145-
const { cachedPolicy, cachedBody } = letsPretendThisIsSomeCache.get(
146-
newRequest.url
147-
);
148-
149-
if (!cachedPolicy.satisfiesWithoutRevalidation(newRequest)) {
150-
// Change the request to ask the origin server if the cached response can be used
151-
newRequest.headers = cachedPolicy.revalidationHeaders(newRequest);
152-
153-
// Send request to the origin server. The server may respond with status 304
154-
const newResponse = await makeRequest(newRequest);
155-
156-
// Create updated policy and combined response from the old and new data
157-
const { policy, modified } = cachedPolicy.revalidatedPolicy(
158-
newRequest,
159-
newResponse
160-
);
161-
const body = modified ? newResponse.body : oldBody;
162-
163-
// Update the cache with the newer/fresher response
164-
letsPretendThisIsSomeCache.set(
165-
newRequest.url,
166-
{ policy, body },
167-
policy.timeToLive()
168-
);
169-
170-
// Make a new response from the cached or revalidated data
171-
return {
172-
headers: policy.responseHeaders()
173-
body,
174-
}
175-
}
176-
```
229+
- `modified` — Boolean indicating whether the response body has changed, and you should use the new response body sent by the server.
230+
- If `true`, you should use the new response body, and you can replace the old cached response with the updated one.
231+
- If `false`, then you should reuse the old cached response body. Either a valid 304 Not Modified response has been received, or an error happened and `stale-if-error` allows falling back to the cache.
177232

178233
# Yo, FRESH
179234

@@ -182,6 +237,7 @@ if (!cachedPolicy.satisfiesWithoutRevalidation(newRequest)) {
182237
## Used by
183238

184239
- [ImageOptim API](https://imageoptim.com/api), [make-fetch-happen](https://github.com/zkat/make-fetch-happen), [cacheable-request](https://www.npmjs.com/package/cacheable-request) ([got](https://www.npmjs.com/package/got)), [npm/registry-fetch](https://github.com/npm/registry-fetch), [etc.](https://github.com/kornelski/http-cache-semantics/network/dependents)
240+
- [Rust version of this library](https://lib.rs/crates/http-cache-semantics).
185241

186242
## Implemented
187243

@@ -195,6 +251,7 @@ if (!cachedPolicy.satisfiesWithoutRevalidation(newRequest)) {
195251
- Filtering of hop-by-hop headers.
196252
- Basic revalidation request
197253
- `stale-if-error`
254+
- `stale-while-revalidate`
198255

199256
## Unimplemented
200257

index.js

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -224,46 +224,69 @@ module.exports = class CachePolicy {
224224
}
225225
}
226226

227+
/**
228+
* Checks if the request matches the cache and can be satisfied from the cache immediately,
229+
* without having to make a request to the server.
230+
*
231+
* This doesn't support `stale-while-revalidate`. See `evaluateRequest()` for a more complete solution.
232+
*/
227233
satisfiesWithoutRevalidation(req) {
228234
const result = this.evaluateRequest(req)
229235
return !result.revalidation;
230236
}
231237

232-
_evaluateRequestHitResult(stale = false, revalidation = undefined) {
238+
_evaluateRequestHitResult(revalidation) {
233239
return {
234240
response: {
235241
headers: this.responseHeaders(),
236-
stale,
237242
},
238243
revalidation,
239244
}
240245
}
241246

242-
_evaluateRequestRevalidation(request, synchronous= true) {
247+
_evaluateRequestRevalidation(request, synchronous) {
243248
return {
244249
synchronous,
245-
request: {
246-
headers: this.revalidationHeaders(request),
247-
},
250+
headers: this.revalidationHeaders(request),
248251
};
249252
}
250253

251254
_evaluateRequestMissResult(request) {
252255
return {
253-
revalidation: this._evaluateRequestRevalidation(request),
256+
response: undefined,
257+
revalidation: this._evaluateRequestRevalidation(request, true),
254258
}
255259
}
256260

261+
/**
262+
* Checks if the given request matches this cache entry, and how the cache can be used to satisfy it. Returns an object with:
263+
*
264+
* ```
265+
* {
266+
* // If defined, you must send a request to the server.
267+
* revalidation: {
268+
* headers: {}, // HTTP headers to use when sending the revalidation response
269+
* // If true, you MUST wait for a response from the server before using the cache
270+
* // If false, this is stale-while-revalidate. The cache is stale, but you can use it while you update it asynchronously.
271+
* synchronous: bool,
272+
* },
273+
* // If defined, you can use this cached response.
274+
* response: {
275+
* headers: {}, // Updated cached HTTP headers you must use when responding to the client
276+
* },
277+
* }
278+
* ```
279+
*/
257280
evaluateRequest(req) {
258281
this._assertRequestHasHeaders(req);
259282

260283
// In all circumstances, a cache MUST NOT ignore the must-revalidate directive
261284
if (this._rescc['must-revalidate']) {
262-
return false;
285+
return this._evaluateRequestMissResult(req);
263286
}
264287

265288
if (!this._requestMatches(req, false)) {
266-
return false;
289+
return this._evaluateRequestMissResult(req);
267290
}
268291

269292
// When presented with a request, a cache MUST NOT reuse a stored response, unless:
@@ -285,27 +308,24 @@ module.exports = class CachePolicy {
285308

286309
// the stored response is either:
287310
// fresh, or allowed to be served stale
288-
let revalidation;
289-
const stale = this.stale();
290-
if (stale) {
291-
let allowsStale = false;
292-
if (requestCC['max-stale'] && !this._rescc['must-revalidate']) {
293-
if (requestCC['max-stale'] === true || requestCC['max-stale'] > this.age() - this.maxAge()) {
294-
allowsStale = true;
295-
}
296-
// Allow stale-while-revalidate queries to be served stale
297-
// even if must-revalidate is set as the revalidation should be happening in the background
298-
} else if (this.useStaleWhileRevalidate()) {
299-
revalidation = this._evaluateRequestRevalidation(req, false);
300-
allowsStale = true;
311+
if (this.stale()) {
312+
// If a value is present, then the client is willing to accept a response that has
313+
// exceeded its freshness lifetime by no more than the specified number of seconds
314+
const allowsStaleWithoutRevalidation = 'max-stale' in requestCC &&
315+
(true === requestCC['max-stale'] || requestCC['max-stale'] > this.age() - this.maxAge());
316+
317+
if (allowsStaleWithoutRevalidation) {
318+
return this._evaluateRequestHitResult(undefined);
301319
}
302320

303-
if (!allowsStale) {
304-
return this._evaluateRequestMissResult(req);
321+
if (this.useStaleWhileRevalidate()) {
322+
return this._evaluateRequestHitResult(this._evaluateRequestRevalidation(req, false));
305323
}
324+
325+
return this._evaluateRequestMissResult(req);
306326
}
307327

308-
return this._evaluateRequestHitResult(stale, revalidation);
328+
return this._evaluateRequestHitResult(undefined);
309329
}
310330

311331
_requestMatches(req, allowHeadMethod) {
@@ -507,6 +527,10 @@ module.exports = class CachePolicy {
507527
return Math.round(Math.max(0, age, staleIfErrorAge, staleWhileRevalidateAge) * 1000);
508528
}
509529

530+
/**
531+
* If true, this cache entry is past its expiration date.
532+
* Note that stale cache may be useful sometimes, see `evaluateRequest()`.
533+
*/
510534
stale() {
511535
return this.maxAge() <= this.age();
512536
}
@@ -515,6 +539,7 @@ module.exports = class CachePolicy {
515539
return this.maxAge() + toNumberOrZero(this._rescc['stale-if-error']) > this.age();
516540
}
517541

542+
/** See `evaluateRequest()` for a more complete solution */
518543
useStaleWhileRevalidate() {
519544
const swr = toNumberOrZero(this._rescc['stale-while-revalidate']);
520545
return swr > 0 && this.maxAge() + swr > this.age();

0 commit comments

Comments
 (0)