b5ffb0a6b36530cdddca9ea20738d005139d085a69f2754471961337efcb9a65d01ed46a88424e6b9b150babd670b1d3ea0435493342701e8d72188bf00cb3 35 KB

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