source: node_modules/http-cache-semantics/index.js@ 81bc7da

finki-main main
Last change on this file since 81bc7da was 81bc7da, checked in by Klimentina Efremova <klimentina08642@…>, 3 months ago

Initial commit

  • Property mode set to 100644
File size: 34.6 KB
Line 
1'use strict';
2
3/**
4 * @typedef {Object} HttpRequest
5 * @property {Record<string, string>} headers - Request headers
6 * @property {string} [method] - HTTP method
7 * @property {string} [url] - Request URL
8 */
9
10/**
11 * @typedef {Object} HttpResponse
12 * @property {Record<string, string>} headers - Response headers
13 * @property {number} [status] - HTTP status code
14 */
15
16/**
17 * Set of default cacheable status codes per RFC 7231 section 6.1.
18 * @type {Set<number>}
19 */
20const statusCodeCacheableByDefault = new Set([
21 200,
22 203,
23 204,
24 206,
25 300,
26 301,
27 308,
28 404,
29 405,
30 410,
31 414,
32 501,
33]);
34
35/**
36 * Set of HTTP status codes that the cache implementation understands.
37 * Note: This implementation does not understand partial responses (206).
38 * @type {Set<number>}
39 */
40const understoodStatuses = new Set([
41 200,
42 203,
43 204,
44 300,
45 301,
46 302,
47 303,
48 307,
49 308,
50 404,
51 405,
52 410,
53 414,
54 501,
55]);
56
57/**
58 * Set of HTTP error status codes.
59 * @type {Set<number>}
60 */
61const errorStatusCodes = new Set([
62 500,
63 502,
64 503,
65 504,
66]);
67
68/**
69 * Object representing hop-by-hop headers that should be removed.
70 * @type {Record<string, boolean>}
71 */
72const hopByHopHeaders = {
73 date: true, // included, because we add Age update Date
74 connection: true,
75 'keep-alive': true,
76 'proxy-authenticate': true,
77 'proxy-authorization': true,
78 te: true,
79 trailer: true,
80 'transfer-encoding': true,
81 upgrade: true,
82};
83
84/**
85 * Headers that are excluded from revalidation update.
86 * @type {Record<string, boolean>}
87 */
88const excludedFromRevalidationUpdate = {
89 // Since the old body is reused, it doesn't make sense to change properties of the body
90 'content-length': true,
91 'content-encoding': true,
92 'transfer-encoding': true,
93 'content-range': true,
94};
95
96/**
97 * Converts a string to a number or returns zero if the conversion fails.
98 * @param {string} s - The string to convert.
99 * @returns {number} The parsed number or 0.
100 */
101function toNumberOrZero(s) {
102 const n = parseInt(s, 10);
103 return isFinite(n) ? n : 0;
104}
105
106/**
107 * Determines if the given response is an error response.
108 * Implements RFC 5861 behavior.
109 * @param {HttpResponse|undefined} response - The HTTP response object.
110 * @returns {boolean} true if the response is an error or undefined, false otherwise.
111 */
112function isErrorResponse(response) {
113 // consider undefined response as faulty
114 if (!response) {
115 return true;
116 }
117 return errorStatusCodes.has(response.status);
118}
119
120/**
121 * Parses a Cache-Control header string into an object.
122 * @param {string} [header] - The Cache-Control header value.
123 * @returns {Record<string, string|boolean>} An object representing Cache-Control directives.
124 */
125function parseCacheControl(header) {
126 /** @type {Record<string, string|boolean>} */
127 const cc = {};
128 if (!header) return cc;
129
130 // TODO: When there is more than one value present for a given directive (e.g., two Expires header fields, multiple Cache-Control: max-age directives),
131 // the directive's value is considered invalid. Caches are encouraged to consider responses that have invalid freshness information to be stale
132 const parts = header.trim().split(/,/);
133 for (const part of parts) {
134 const [k, v] = part.split(/=/, 2);
135 cc[k.trim()] = v === undefined ? true : v.trim().replace(/^"|"$/g, '');
136 }
137
138 return cc;
139}
140
141/**
142 * Formats a Cache-Control directives object into a header string.
143 * @param {Record<string, string|boolean>} cc - The Cache-Control directives.
144 * @returns {string|undefined} A formatted Cache-Control header string or undefined if empty.
145 */
146function formatCacheControl(cc) {
147 let parts = [];
148 for (const k in cc) {
149 const v = cc[k];
150 parts.push(v === true ? k : k + '=' + v);
151 }
152 if (!parts.length) {
153 return undefined;
154 }
155 return parts.join(', ');
156}
157
158module.exports = class CachePolicy {
159 /**
160 * Creates a new CachePolicy instance.
161 * @param {HttpRequest} req - Incoming client request.
162 * @param {HttpResponse} res - Received server response.
163 * @param {Object} [options={}] - Configuration options.
164 * @param {boolean} [options.shared=true] - Is the cache shared (a public proxy)? `false` for personal browser caches.
165 * @param {number} [options.cacheHeuristic=0.1] - Fallback heuristic (age fraction) for cache duration.
166 * @param {number} [options.immutableMinTimeToLive=86400000] - Minimum TTL for immutable responses in milliseconds.
167 * @param {boolean} [options.ignoreCargoCult=false] - Detect nonsense cache headers, and override them.
168 * @param {any} [options._fromObject] - Internal parameter for deserialization. Do not use.
169 */
170 constructor(
171 req,
172 res,
173 {
174 shared,
175 cacheHeuristic,
176 immutableMinTimeToLive,
177 ignoreCargoCult,
178 _fromObject,
179 } = {}
180 ) {
181 if (_fromObject) {
182 this._fromObject(_fromObject);
183 return;
184 }
185
186 if (!res || !res.headers) {
187 throw Error('Response headers missing');
188 }
189 this._assertRequestHasHeaders(req);
190
191 /** @type {number} Timestamp when the response was received */
192 this._responseTime = this.now();
193 /** @type {boolean} Indicates if the cache is shared */
194 this._isShared = shared !== false;
195 /** @type {boolean} Indicates if legacy cargo cult directives should be ignored */
196 this._ignoreCargoCult = !!ignoreCargoCult;
197 /** @type {number} Heuristic cache fraction */
198 this._cacheHeuristic =
199 undefined !== cacheHeuristic ? cacheHeuristic : 0.1; // 10% matches IE
200 /** @type {number} Minimum TTL for immutable responses in ms */
201 this._immutableMinTtl =
202 undefined !== immutableMinTimeToLive
203 ? immutableMinTimeToLive
204 : 24 * 3600 * 1000;
205
206 /** @type {number} HTTP status code */
207 this._status = 'status' in res ? res.status : 200;
208 /** @type {Record<string, string>} Response headers */
209 this._resHeaders = res.headers;
210 /** @type {Record<string, string|boolean>} Parsed Cache-Control directives from response */
211 this._rescc = parseCacheControl(res.headers['cache-control']);
212 /** @type {string} HTTP method (e.g., GET, POST) */
213 this._method = 'method' in req ? req.method : 'GET';
214 /** @type {string} Request URL */
215 this._url = req.url;
216 /** @type {string} Host header from the request */
217 this._host = req.headers.host;
218 /** @type {boolean} Whether the request does not include an Authorization header */
219 this._noAuthorization = !req.headers.authorization;
220 /** @type {Record<string, string>|null} Request headers used for Vary matching */
221 this._reqHeaders = res.headers.vary ? req.headers : null; // Don't keep all request headers if they won't be used
222 /** @type {Record<string, string|boolean>} Parsed Cache-Control directives from request */
223 this._reqcc = parseCacheControl(req.headers['cache-control']);
224
225 // Assume that if someone uses legacy, non-standard uncecessary options they don't understand caching,
226 // so there's no point stricly adhering to the blindly copy&pasted directives.
227 if (
228 this._ignoreCargoCult &&
229 'pre-check' in this._rescc &&
230 'post-check' in this._rescc
231 ) {
232 delete this._rescc['pre-check'];
233 delete this._rescc['post-check'];
234 delete this._rescc['no-cache'];
235 delete this._rescc['no-store'];
236 delete this._rescc['must-revalidate'];
237 this._resHeaders = Object.assign({}, this._resHeaders, {
238 'cache-control': formatCacheControl(this._rescc),
239 });
240 delete this._resHeaders.expires;
241 delete this._resHeaders.pragma;
242 }
243
244 // When the Cache-Control header field is not present in a request, caches MUST consider the no-cache request pragma-directive
245 // as having the same effect as if "Cache-Control: no-cache" were present (see Section 5.2.1).
246 if (
247 res.headers['cache-control'] == null &&
248 /no-cache/.test(res.headers.pragma)
249 ) {
250 this._rescc['no-cache'] = true;
251 }
252 }
253
254 /**
255 * You can monkey-patch it for testing.
256 * @returns {number} Current time in milliseconds.
257 */
258 now() {
259 return Date.now();
260 }
261
262 /**
263 * Determines if the response is storable in a cache.
264 * @returns {boolean} `false` if can never be cached.
265 */
266 storable() {
267 // The "no-store" request directive indicates that a cache MUST NOT store any part of either this request or any response to it.
268 return !!(
269 !this._reqcc['no-store'] &&
270 // A cache MUST NOT store a response to any request, unless:
271 // The request method is understood by the cache and defined as being cacheable, and
272 ('GET' === this._method ||
273 'HEAD' === this._method ||
274 ('POST' === this._method && this._hasExplicitExpiration())) &&
275 // the response status code is understood by the cache, and
276 understoodStatuses.has(this._status) &&
277 // the "no-store" cache directive does not appear in request or response header fields, and
278 !this._rescc['no-store'] &&
279 // the "private" response directive does not appear in the response, if the cache is shared, and
280 (!this._isShared || !this._rescc.private) &&
281 // the Authorization header field does not appear in the request, if the cache is shared,
282 (!this._isShared ||
283 this._noAuthorization ||
284 this._allowsStoringAuthenticated()) &&
285 // the response either:
286 // contains an Expires header field, or
287 (this._resHeaders.expires ||
288 // contains a max-age response directive, or
289 // contains a s-maxage response directive and the cache is shared, or
290 // contains a public response directive.
291 this._rescc['max-age'] ||
292 (this._isShared && this._rescc['s-maxage']) ||
293 this._rescc.public ||
294 // has a status code that is defined as cacheable by default
295 statusCodeCacheableByDefault.has(this._status))
296 );
297 }
298
299 /**
300 * @returns {boolean} true if expiration is explicitly defined.
301 */
302 _hasExplicitExpiration() {
303 // 4.2.1 Calculating Freshness Lifetime
304 return !!(
305 (this._isShared && this._rescc['s-maxage']) ||
306 this._rescc['max-age'] ||
307 this._resHeaders.expires
308 );
309 }
310
311 /**
312 * @param {HttpRequest} req - a request
313 * @throws {Error} if the headers are missing.
314 */
315 _assertRequestHasHeaders(req) {
316 if (!req || !req.headers) {
317 throw Error('Request headers missing');
318 }
319 }
320
321 /**
322 * Checks if the request matches the cache and can be satisfied from the cache immediately,
323 * without having to make a request to the server.
324 *
325 * This doesn't support `stale-while-revalidate`. See `evaluateRequest()` for a more complete solution.
326 *
327 * @param {HttpRequest} req - The new incoming HTTP request.
328 * @returns {boolean} `true`` if the cached response used to construct this cache policy satisfies the request without revalidation.
329 */
330 satisfiesWithoutRevalidation(req) {
331 const result = this.evaluateRequest(req);
332 return !result.revalidation;
333 }
334
335 /**
336 * @param {{headers: Record<string, string>, synchronous: boolean}|undefined} revalidation - Revalidation information, if any.
337 * @returns {{response: {headers: Record<string, string>}, revalidation: {headers: Record<string, string>, synchronous: boolean}|undefined}} An object with a cached response headers and revalidation info.
338 */
339 _evaluateRequestHitResult(revalidation) {
340 return {
341 response: {
342 headers: this.responseHeaders(),
343 },
344 revalidation,
345 };
346 }
347
348 /**
349 * @param {HttpRequest} request - new incoming
350 * @param {boolean} synchronous - whether revalidation must be synchronous (not s-w-r).
351 * @returns {{headers: Record<string, string>, synchronous: boolean}} An object with revalidation headers and a synchronous flag.
352 */
353 _evaluateRequestRevalidation(request, synchronous) {
354 return {
355 synchronous,
356 headers: this.revalidationHeaders(request),
357 };
358 }
359
360 /**
361 * @param {HttpRequest} request - new incoming
362 * @returns {{response: undefined, revalidation: {headers: Record<string, string>, synchronous: boolean}}} An object indicating no cached response and revalidation details.
363 */
364 _evaluateRequestMissResult(request) {
365 return {
366 response: undefined,
367 revalidation: this._evaluateRequestRevalidation(request, true),
368 };
369 }
370
371 /**
372 * Checks if the given request matches this cache entry, and how the cache can be used to satisfy it. Returns an object with:
373 *
374 * ```
375 * {
376 * // If defined, you must send a request to the server.
377 * revalidation: {
378 * headers: {}, // HTTP headers to use when sending the revalidation response
379 * // If true, you MUST wait for a response from the server before using the cache
380 * // If false, this is stale-while-revalidate. The cache is stale, but you can use it while you update it asynchronously.
381 * synchronous: bool,
382 * },
383 * // If defined, you can use this cached response.
384 * response: {
385 * headers: {}, // Updated cached HTTP headers you must use when responding to the client
386 * },
387 * }
388 * ```
389 * @param {HttpRequest} req - new incoming HTTP request
390 * @returns {{response: {headers: Record<string, string>}|undefined, revalidation: {headers: Record<string, string>, synchronous: boolean}|undefined}} An object containing keys:
391 * - revalidation: { headers: Record<string, string>, synchronous: boolean } Set if you should send this to the origin server
392 * - response: { headers: Record<string, string> } Set if you can respond to the client with these cached headers
393 */
394 evaluateRequest(req) {
395 this._assertRequestHasHeaders(req);
396
397 // In all circumstances, a cache MUST NOT ignore the must-revalidate directive
398 if (this._rescc['must-revalidate']) {
399 return this._evaluateRequestMissResult(req);
400 }
401
402 if (!this._requestMatches(req, false)) {
403 return this._evaluateRequestMissResult(req);
404 }
405
406 // When presented with a request, a cache MUST NOT reuse a stored response, unless:
407 // the presented request does not contain the no-cache pragma (Section 5.4), nor the no-cache cache directive,
408 // unless the stored response is successfully validated (Section 4.3), and
409 const requestCC = parseCacheControl(req.headers['cache-control']);
410
411 if (requestCC['no-cache'] || /no-cache/.test(req.headers.pragma)) {
412 return this._evaluateRequestMissResult(req);
413 }
414
415 if (requestCC['max-age'] && this.age() > toNumberOrZero(requestCC['max-age'])) {
416 return this._evaluateRequestMissResult(req);
417 }
418
419 if (requestCC['min-fresh'] && this.maxAge() - this.age() < toNumberOrZero(requestCC['min-fresh'])) {
420 return this._evaluateRequestMissResult(req);
421 }
422
423 // the stored response is either:
424 // fresh, or allowed to be served stale
425 if (this.stale()) {
426 // If a value is present, then the client is willing to accept a response that has
427 // exceeded its freshness lifetime by no more than the specified number of seconds
428 const allowsStaleWithoutRevalidation = 'max-stale' in requestCC &&
429 (true === requestCC['max-stale'] || requestCC['max-stale'] > this.age() - this.maxAge());
430
431 if (allowsStaleWithoutRevalidation) {
432 return this._evaluateRequestHitResult(undefined);
433 }
434
435 if (this.useStaleWhileRevalidate()) {
436 return this._evaluateRequestHitResult(this._evaluateRequestRevalidation(req, false));
437 }
438
439 return this._evaluateRequestMissResult(req);
440 }
441
442 return this._evaluateRequestHitResult(undefined);
443 }
444
445 /**
446 * @param {HttpRequest} req - check if this is for the same cache entry
447 * @param {boolean} allowHeadMethod - allow a HEAD method to match.
448 * @returns {boolean} `true` if the request matches.
449 */
450 _requestMatches(req, allowHeadMethod) {
451 // The presented effective request URI and that of the stored response match, and
452 return !!(
453 (!this._url || this._url === req.url) &&
454 this._host === req.headers.host &&
455 // the request method associated with the stored response allows it to be used for the presented request, and
456 (!req.method ||
457 this._method === req.method ||
458 (allowHeadMethod && 'HEAD' === req.method)) &&
459 // selecting header fields nominated by the stored response (if any) match those presented, and
460 this._varyMatches(req)
461 );
462 }
463
464 /**
465 * Determines whether storing authenticated responses is allowed.
466 * @returns {boolean} `true` if allowed.
467 */
468 _allowsStoringAuthenticated() {
469 // following Cache-Control response directives (Section 5.2.2) have such an effect: must-revalidate, public, and s-maxage.
470 return !!(
471 this._rescc['must-revalidate'] ||
472 this._rescc.public ||
473 this._rescc['s-maxage']
474 );
475 }
476
477 /**
478 * Checks whether the Vary header in the response matches the new request.
479 * @param {HttpRequest} req - incoming HTTP request
480 * @returns {boolean} `true` if the vary headers match.
481 */
482 _varyMatches(req) {
483 if (!this._resHeaders.vary) {
484 return true;
485 }
486
487 // A Vary header field-value of "*" always fails to match
488 if (this._resHeaders.vary === '*') {
489 return false;
490 }
491
492 const fields = this._resHeaders.vary
493 .trim()
494 .toLowerCase()
495 .split(/\s*,\s*/);
496 for (const name of fields) {
497 if (req.headers[name] !== this._reqHeaders[name]) return false;
498 }
499 return true;
500 }
501
502 /**
503 * Creates a copy of the given headers without any hop-by-hop headers.
504 * @param {Record<string, string>} inHeaders - old headers from the cached response
505 * @returns {Record<string, string>} A new headers object without hop-by-hop headers.
506 */
507 _copyWithoutHopByHopHeaders(inHeaders) {
508 /** @type {Record<string, string>} */
509 const headers = {};
510 for (const name in inHeaders) {
511 if (hopByHopHeaders[name]) continue;
512 headers[name] = inHeaders[name];
513 }
514 // 9.1. Connection
515 if (inHeaders.connection) {
516 const tokens = inHeaders.connection.trim().split(/\s*,\s*/);
517 for (const name of tokens) {
518 delete headers[name];
519 }
520 }
521 if (headers.warning) {
522 const warnings = headers.warning.split(/,/).filter(warning => {
523 return !/^\s*1[0-9][0-9]/.test(warning);
524 });
525 if (!warnings.length) {
526 delete headers.warning;
527 } else {
528 headers.warning = warnings.join(',').trim();
529 }
530 }
531 return headers;
532 }
533
534 /**
535 * Returns the response headers adjusted for serving the cached response.
536 * Removes hop-by-hop headers and updates the Age and Date headers.
537 * @returns {Record<string, string>} The adjusted response headers.
538 */
539 responseHeaders() {
540 const headers = this._copyWithoutHopByHopHeaders(this._resHeaders);
541 const age = this.age();
542
543 // A cache SHOULD generate 113 warning if it heuristically chose a freshness
544 // lifetime greater than 24 hours and the response's age is greater than 24 hours.
545 if (
546 age > 3600 * 24 &&
547 !this._hasExplicitExpiration() &&
548 this.maxAge() > 3600 * 24
549 ) {
550 headers.warning =
551 (headers.warning ? `${headers.warning}, ` : '') +
552 '113 - "rfc7234 5.5.4"';
553 }
554 headers.age = `${Math.round(age)}`;
555 headers.date = new Date(this.now()).toUTCString();
556 return headers;
557 }
558
559 /**
560 * Returns the Date header value from the response or the current time if invalid.
561 * @returns {number} Timestamp (in milliseconds) representing the Date header or response time.
562 */
563 date() {
564 const serverDate = Date.parse(this._resHeaders.date);
565 if (isFinite(serverDate)) {
566 return serverDate;
567 }
568 return this._responseTime;
569 }
570
571 /**
572 * Value of the Age header, in seconds, updated for the current time.
573 * May be fractional.
574 * @returns {number} The age in seconds.
575 */
576 age() {
577 let age = this._ageValue();
578
579 const residentTime = (this.now() - this._responseTime) / 1000;
580 return age + residentTime;
581 }
582
583 /**
584 * @returns {number} The Age header value as a number.
585 */
586 _ageValue() {
587 return toNumberOrZero(this._resHeaders.age);
588 }
589
590 /**
591 * Possibly outdated value of applicable max-age (or heuristic equivalent) in seconds.
592 * This counts since response's `Date`.
593 *
594 * For an up-to-date value, see `timeToLive()`.
595 *
596 * Returns the maximum age (freshness lifetime) of the response in seconds.
597 * @returns {number} The max-age value in seconds.
598 */
599 maxAge() {
600 if (!this.storable() || this._rescc['no-cache']) {
601 return 0;
602 }
603
604 // Shared responses with cookies are cacheable according to the RFC, but IMHO it'd be unwise to do so by default
605 // so this implementation requires explicit opt-in via public header
606 if (
607 this._isShared &&
608 (this._resHeaders['set-cookie'] &&
609 !this._rescc.public &&
610 !this._rescc.immutable)
611 ) {
612 return 0;
613 }
614
615 if (this._resHeaders.vary === '*') {
616 return 0;
617 }
618
619 if (this._isShared) {
620 if (this._rescc['proxy-revalidate']) {
621 return 0;
622 }
623 // if a response includes the s-maxage directive, a shared cache recipient MUST ignore the Expires field.
624 if (this._rescc['s-maxage']) {
625 return toNumberOrZero(this._rescc['s-maxage']);
626 }
627 }
628
629 // If a response includes a Cache-Control field with the max-age directive, a recipient MUST ignore the Expires field.
630 if (this._rescc['max-age']) {
631 return toNumberOrZero(this._rescc['max-age']);
632 }
633
634 const defaultMinTtl = this._rescc.immutable ? this._immutableMinTtl : 0;
635
636 const serverDate = this.date();
637 if (this._resHeaders.expires) {
638 const expires = Date.parse(this._resHeaders.expires);
639 // A cache recipient MUST interpret invalid date formats, especially the value "0", as representing a time in the past (i.e., "already expired").
640 if (Number.isNaN(expires) || expires < serverDate) {
641 return 0;
642 }
643 return Math.max(defaultMinTtl, (expires - serverDate) / 1000);
644 }
645
646 if (this._resHeaders['last-modified']) {
647 const lastModified = Date.parse(this._resHeaders['last-modified']);
648 if (isFinite(lastModified) && serverDate > lastModified) {
649 return Math.max(
650 defaultMinTtl,
651 ((serverDate - lastModified) / 1000) * this._cacheHeuristic
652 );
653 }
654 }
655
656 return defaultMinTtl;
657 }
658
659 /**
660 * Remaining time this cache entry may be useful for, in *milliseconds*.
661 * You can use this as an expiration time for your cache storage.
662 *
663 * Prefer this method over `maxAge()`, because it includes other factors like `age` and `stale-while-revalidate`.
664 * @returns {number} Time-to-live in milliseconds.
665 */
666 timeToLive() {
667 const age = this.maxAge() - this.age();
668 const staleIfErrorAge = age + toNumberOrZero(this._rescc['stale-if-error']);
669 const staleWhileRevalidateAge = age + toNumberOrZero(this._rescc['stale-while-revalidate']);
670 return Math.round(Math.max(0, age, staleIfErrorAge, staleWhileRevalidateAge) * 1000);
671 }
672
673 /**
674 * If true, this cache entry is past its expiration date.
675 * Note that stale cache may be useful sometimes, see `evaluateRequest()`.
676 * @returns {boolean} `false` doesn't mean it's fresh nor usable
677 */
678 stale() {
679 return this.maxAge() <= this.age();
680 }
681
682 /**
683 * @returns {boolean} `true` if `stale-if-error` condition allows use of a stale response.
684 */
685 _useStaleIfError() {
686 return this.maxAge() + toNumberOrZero(this._rescc['stale-if-error']) > this.age();
687 }
688
689 /** See `evaluateRequest()` for a more complete solution
690 * @returns {boolean} `true` if `stale-while-revalidate` is currently allowed.
691 */
692 useStaleWhileRevalidate() {
693 const swr = toNumberOrZero(this._rescc['stale-while-revalidate']);
694 return swr > 0 && this.maxAge() + swr > this.age();
695 }
696
697 /**
698 * Creates a `CachePolicy` instance from a serialized object.
699 * @param {Object} obj - The serialized object.
700 * @returns {CachePolicy} A new CachePolicy instance.
701 */
702 static fromObject(obj) {
703 return new this(undefined, undefined, { _fromObject: obj });
704 }
705
706 /**
707 * @param {any} obj - The serialized object.
708 * @throws {Error} If already initialized or if the object is invalid.
709 */
710 _fromObject(obj) {
711 if (this._responseTime) throw Error('Reinitialized');
712 if (!obj || obj.v !== 1) throw Error('Invalid serialization');
713
714 this._responseTime = obj.t;
715 this._isShared = obj.sh;
716 this._cacheHeuristic = obj.ch;
717 this._immutableMinTtl =
718 obj.imm !== undefined ? obj.imm : 24 * 3600 * 1000;
719 this._ignoreCargoCult = !!obj.icc;
720 this._status = obj.st;
721 this._resHeaders = obj.resh;
722 this._rescc = obj.rescc;
723 this._method = obj.m;
724 this._url = obj.u;
725 this._host = obj.h;
726 this._noAuthorization = obj.a;
727 this._reqHeaders = obj.reqh;
728 this._reqcc = obj.reqcc;
729 }
730
731 /**
732 * Serializes the `CachePolicy` instance into a JSON-serializable object.
733 * @returns {Object} The serialized object.
734 */
735 toObject() {
736 return {
737 v: 1,
738 t: this._responseTime,
739 sh: this._isShared,
740 ch: this._cacheHeuristic,
741 imm: this._immutableMinTtl,
742 icc: this._ignoreCargoCult,
743 st: this._status,
744 resh: this._resHeaders,
745 rescc: this._rescc,
746 m: this._method,
747 u: this._url,
748 h: this._host,
749 a: this._noAuthorization,
750 reqh: this._reqHeaders,
751 reqcc: this._reqcc,
752 };
753 }
754
755 /**
756 * Headers for sending to the origin server to revalidate stale response.
757 * Allows server to return 304 to allow reuse of the previous response.
758 *
759 * Hop by hop headers are always stripped.
760 * Revalidation headers may be added or removed, depending on request.
761 * @param {HttpRequest} incomingReq - The incoming HTTP request.
762 * @returns {Record<string, string>} The headers for the revalidation request.
763 */
764 revalidationHeaders(incomingReq) {
765 this._assertRequestHasHeaders(incomingReq);
766 const headers = this._copyWithoutHopByHopHeaders(incomingReq.headers);
767
768 // This implementation does not understand range requests
769 delete headers['if-range'];
770
771 if (!this._requestMatches(incomingReq, true) || !this.storable()) {
772 // revalidation allowed via HEAD
773 // not for the same resource, or wasn't allowed to be cached anyway
774 delete headers['if-none-match'];
775 delete headers['if-modified-since'];
776 return headers;
777 }
778
779 /* MUST send that entity-tag in any cache validation request (using If-Match or If-None-Match) if an entity-tag has been provided by the origin server. */
780 if (this._resHeaders.etag) {
781 headers['if-none-match'] = headers['if-none-match']
782 ? `${headers['if-none-match']}, ${this._resHeaders.etag}`
783 : this._resHeaders.etag;
784 }
785
786 // Clients MAY issue simple (non-subrange) GET requests with either weak validators or strong validators. Clients MUST NOT use weak validators in other forms of request.
787 const forbidsWeakValidators =
788 headers['accept-ranges'] ||
789 headers['if-match'] ||
790 headers['if-unmodified-since'] ||
791 (this._method && this._method != 'GET');
792
793 /* SHOULD send the Last-Modified value in non-subrange cache validation requests (using If-Modified-Since) if only a Last-Modified value has been provided by the origin server.
794 Note: This implementation does not understand partial responses (206) */
795 if (forbidsWeakValidators) {
796 delete headers['if-modified-since'];
797
798 if (headers['if-none-match']) {
799 const etags = headers['if-none-match']
800 .split(/,/)
801 .filter(etag => {
802 return !/^\s*W\//.test(etag);
803 });
804 if (!etags.length) {
805 delete headers['if-none-match'];
806 } else {
807 headers['if-none-match'] = etags.join(',').trim();
808 }
809 }
810 } else if (
811 this._resHeaders['last-modified'] &&
812 !headers['if-modified-since']
813 ) {
814 headers['if-modified-since'] = this._resHeaders['last-modified'];
815 }
816
817 return headers;
818 }
819
820 /**
821 * Creates new CachePolicy with information combined from the previews response,
822 * and the new revalidation response.
823 *
824 * Returns {policy, modified} where modified is a boolean indicating
825 * whether the response body has been modified, and old cached body can't be used.
826 *
827 * @param {HttpRequest} request - The latest HTTP request asking for the cached entry.
828 * @param {HttpResponse} response - The latest revalidation HTTP response from the origin server.
829 * @returns {{policy: CachePolicy, modified: boolean, matches: boolean}} The updated policy and modification status.
830 * @throws {Error} If the response headers are missing.
831 */
832 revalidatedPolicy(request, response) {
833 this._assertRequestHasHeaders(request);
834
835 if (this._useStaleIfError() && isErrorResponse(response)) {
836 return {
837 policy: this,
838 modified: false,
839 matches: true,
840 };
841 }
842
843 if (!response || !response.headers) {
844 throw Error('Response headers missing');
845 }
846
847 // These aren't going to be supported exactly, since one CachePolicy object
848 // doesn't know about all the other cached objects.
849 let matches = false;
850 if (response.status !== undefined && response.status != 304) {
851 matches = false;
852 } else if (
853 response.headers.etag &&
854 !/^\s*W\//.test(response.headers.etag)
855 ) {
856 // "All of the stored responses with the same strong validator are selected.
857 // If none of the stored responses contain the same strong validator,
858 // then the cache MUST NOT use the new response to update any stored responses."
859 matches =
860 this._resHeaders.etag &&
861 this._resHeaders.etag.replace(/^\s*W\//, '') ===
862 response.headers.etag;
863 } else if (this._resHeaders.etag && response.headers.etag) {
864 // "If the new response contains a weak validator and that validator corresponds
865 // to one of the cache's stored responses,
866 // then the most recent of those matching stored responses is selected for update."
867 matches =
868 this._resHeaders.etag.replace(/^\s*W\//, '') ===
869 response.headers.etag.replace(/^\s*W\//, '');
870 } else if (this._resHeaders['last-modified']) {
871 matches =
872 this._resHeaders['last-modified'] ===
873 response.headers['last-modified'];
874 } else {
875 // If the new response does not include any form of validator (such as in the case where
876 // a client generates an If-Modified-Since request from a source other than the Last-Modified
877 // response header field), and there is only one stored response, and that stored response also
878 // lacks a validator, then that stored response is selected for update.
879 if (
880 !this._resHeaders.etag &&
881 !this._resHeaders['last-modified'] &&
882 !response.headers.etag &&
883 !response.headers['last-modified']
884 ) {
885 matches = true;
886 }
887 }
888
889 const optionsCopy = {
890 shared: this._isShared,
891 cacheHeuristic: this._cacheHeuristic,
892 immutableMinTimeToLive: this._immutableMinTtl,
893 ignoreCargoCult: this._ignoreCargoCult,
894 };
895
896 if (!matches) {
897 return {
898 policy: new this.constructor(request, response, optionsCopy),
899 // Client receiving 304 without body, even if it's invalid/mismatched has no option
900 // but to reuse a cached body. We don't have a good way to tell clients to do
901 // error recovery in such case.
902 modified: response.status != 304,
903 matches: false,
904 };
905 }
906
907 // use other header fields provided in the 304 (Not Modified) response to replace all instances
908 // of the corresponding header fields in the stored response.
909 const headers = {};
910 for (const k in this._resHeaders) {
911 headers[k] =
912 k in response.headers && !excludedFromRevalidationUpdate[k]
913 ? response.headers[k]
914 : this._resHeaders[k];
915 }
916
917 const newResponse = Object.assign({}, response, {
918 status: this._status,
919 method: this._method,
920 headers,
921 });
922 return {
923 policy: new this.constructor(request, newResponse, optionsCopy),
924 modified: false,
925 matches: true,
926 };
927 }
928};
Note: See TracBrowser for help on using the repository browser.