cache-handler.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776
  1. 'use strict'
  2. const util = require('../core/util')
  3. const {
  4. parseCacheControlHeader,
  5. hasInvalidCacheControlDirective,
  6. parseVaryHeader,
  7. hasVaryStar,
  8. isInvalidOrWildcardVaryHeader,
  9. isEtagUsable
  10. } = require('../util/cache')
  11. const { parseHttpDate } = require('../util/date.js')
  12. function noop () {}
  13. // Status codes that we can use some heuristics on to cache
  14. const HEURISTICALLY_CACHEABLE_STATUS_CODES = [
  15. 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501
  16. ]
  17. // Status codes which semantic is not handled by the cache
  18. // https://datatracker.ietf.org/doc/html/rfc9111#section-3
  19. // This list should not grow beyond 206 unless the RFC is updated
  20. // by a newer one including more. Please introduce another list if
  21. // implementing caching of responses with the 'must-understand' directive.
  22. const NOT_UNDERSTOOD_STATUS_CODES = [
  23. 206
  24. ]
  25. const MAX_RESPONSE_AGE = 2147483647000
  26. function trimOWS (value) {
  27. return value.replace(/^[\t ]+|[\t ]+$/g, '')
  28. }
  29. function arrayIncludes (array, value) {
  30. for (let i = 0; i < array.length; i++) {
  31. if (array[i] === value) {
  32. return true
  33. }
  34. }
  35. return false
  36. }
  37. function appendConnectionHeaderTokens (headersToRemove, connectionHeader) {
  38. const values = Array.isArray(connectionHeader) ? connectionHeader : [connectionHeader]
  39. for (let i = 0; i < values.length; i++) {
  40. const tokens = values[i].split(',')
  41. for (let j = 0; j < tokens.length; j++) {
  42. headersToRemove.push(trimOWS(tokens[j]).toLowerCase())
  43. }
  44. }
  45. }
  46. function getSameOriginPath (cacheKey, location) {
  47. if (typeof location !== 'string') {
  48. return undefined
  49. }
  50. let originUrl
  51. let requestUrl
  52. let locationUrl
  53. try {
  54. originUrl = new URL(cacheKey.origin)
  55. requestUrl = new URL(cacheKey.path, originUrl)
  56. locationUrl = new URL(location, requestUrl)
  57. } catch {
  58. return undefined
  59. }
  60. if (locationUrl.origin !== originUrl.origin) {
  61. return undefined
  62. }
  63. return locationUrl.pathname + locationUrl.search
  64. }
  65. function deleteCachedUri (store, cacheKey, path) {
  66. deleteCachedValue(store, {
  67. ...cacheKey,
  68. path
  69. })
  70. for (let i = 0; i < util.safeHTTPMethods.length; i++) {
  71. const method = util.safeHTTPMethods[i]
  72. if (method !== cacheKey.method) {
  73. deleteCachedValue(store, {
  74. ...cacheKey,
  75. method,
  76. path
  77. })
  78. }
  79. }
  80. }
  81. function deleteLocationTargets (store, cacheKey, headerValue) {
  82. if (headerValue === undefined) {
  83. return
  84. }
  85. const values = Array.isArray(headerValue) ? headerValue : [headerValue]
  86. for (let i = 0; i < values.length; i++) {
  87. const path = getSameOriginPath(cacheKey, values[i])
  88. if (path !== undefined) {
  89. deleteCachedUri(store, cacheKey, path)
  90. }
  91. }
  92. }
  93. function invalidateUnsafeRequest (store, cacheKey, resHeaders) {
  94. deleteCachedUri(store, cacheKey, cacheKey.path)
  95. deleteLocationTargets(store, cacheKey, resHeaders.location)
  96. deleteLocationTargets(store, cacheKey, resHeaders['content-location'])
  97. }
  98. /**
  99. * @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler
  100. *
  101. * @implements {DispatchHandler}
  102. */
  103. class CacheHandler {
  104. /**
  105. * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
  106. */
  107. #cacheKey
  108. /**
  109. * @type {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions['type']}
  110. */
  111. #cacheType
  112. /**
  113. * @type {number | undefined}
  114. */
  115. #cacheByDefault
  116. /**
  117. * @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
  118. */
  119. #store
  120. /**
  121. * @type {import('../../types/dispatcher.d.ts').default.DispatchHandler}
  122. */
  123. #handler
  124. /**
  125. * @type {import('node:stream').Writable | undefined}
  126. */
  127. #writeStream
  128. /**
  129. * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} opts
  130. * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
  131. * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
  132. */
  133. constructor ({ store, type, cacheByDefault }, cacheKey, handler) {
  134. this.#store = store
  135. this.#cacheType = type
  136. this.#cacheByDefault = cacheByDefault
  137. this.#cacheKey = cacheKey
  138. this.#handler = handler
  139. }
  140. onRequestStart (controller, context) {
  141. this.#writeStream?.destroy()
  142. this.#writeStream = undefined
  143. this.#handler.onRequestStart?.(controller, context)
  144. }
  145. onRequestUpgrade (controller, statusCode, headers, socket) {
  146. this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
  147. }
  148. /**
  149. * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
  150. * @param {number} statusCode
  151. * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
  152. * @param {string} statusMessage
  153. */
  154. onResponseStart (
  155. controller,
  156. statusCode,
  157. resHeaders,
  158. statusMessage
  159. ) {
  160. const downstreamOnHeaders = () =>
  161. this.#handler.onResponseStart?.(
  162. controller,
  163. statusCode,
  164. resHeaders,
  165. statusMessage
  166. )
  167. const handler = this
  168. if (
  169. !arrayIncludes(util.safeHTTPMethods, this.#cacheKey.method) &&
  170. statusCode >= 200 &&
  171. statusCode <= 399
  172. ) {
  173. // Successful response to an unsafe method, delete it from cache
  174. // https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-response
  175. invalidateUnsafeRequest(this.#store, this.#cacheKey, resHeaders)
  176. return downstreamOnHeaders()
  177. }
  178. const cacheControlHeader = resHeaders['cache-control']
  179. const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
  180. if (revalidationResponseDisallowsCachedReuse(this.#cacheType, resHeaders, cacheControlDirectives)) {
  181. deleteCachedValue(this.#store, this.#cacheKey)
  182. return downstreamOnHeaders()
  183. }
  184. const heuristicallyCacheable = resHeaders['last-modified'] && arrayIncludes(HEURISTICALLY_CACHEABLE_STATUS_CODES, statusCode)
  185. if (
  186. !cacheControlHeader &&
  187. !resHeaders['expires'] &&
  188. !heuristicallyCacheable &&
  189. !this.#cacheByDefault
  190. ) {
  191. if (statusCode === 304 && resHeaders.vary && isInvalidOrWildcardVaryHeader(resHeaders.vary)) {
  192. deleteCachedValue(this.#store, this.#cacheKey)
  193. }
  194. // Don't have anything to tell us this response is cachable and we're not
  195. // caching by default
  196. return downstreamOnHeaders()
  197. }
  198. if (!canCacheResponse(this.#cacheType, this.#cacheKey.method, statusCode, resHeaders, cacheControlDirectives, this.#cacheKey.headers)) {
  199. if (statusCode === 304 && (cacheControlHeader || revalidationResponseDisallowsCachedReuse(this.#cacheType, resHeaders, cacheControlDirectives))) {
  200. deleteCachedValue(this.#store, this.#cacheKey)
  201. }
  202. return downstreamOnHeaders()
  203. }
  204. const now = Date.now()
  205. const resAge = Object.hasOwn(resHeaders, 'age') ? getAge(resHeaders.age) : undefined
  206. if (resAge !== undefined && resAge >= MAX_RESPONSE_AGE) {
  207. // Response considered stale
  208. deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey)
  209. return downstreamOnHeaders()
  210. }
  211. const resDate = Object.hasOwn(resHeaders, 'date') ? getDate(resHeaders.date) : undefined
  212. if (resDate === null) {
  213. deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey)
  214. return downstreamOnHeaders()
  215. }
  216. const apparentAge = resDate ? Math.max(0, now - resDate.getTime()) : 0
  217. const currentAge = Math.max(apparentAge, resAge ?? 0)
  218. const staleAt =
  219. determineStaleAt(this.#cacheType, now, resAge, resHeaders, resDate, cacheControlDirectives) ??
  220. this.#cacheByDefault
  221. if (staleAt === undefined || currentAge >= staleAt) {
  222. if (cacheControlHeader || staleAt !== undefined) {
  223. deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey)
  224. }
  225. return downstreamOnHeaders()
  226. }
  227. const baseTime = now - currentAge
  228. const absoluteStaleAt = staleAt + baseTime
  229. if (now >= absoluteStaleAt) {
  230. // Response is already stale
  231. deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey)
  232. return downstreamOnHeaders()
  233. }
  234. let varyDirectives
  235. if (this.#cacheKey.headers && resHeaders.vary) {
  236. varyDirectives = parseVaryHeader(resHeaders.vary, this.#cacheKey.headers)
  237. if (!varyDirectives) {
  238. // Parse error
  239. return downstreamOnHeaders()
  240. }
  241. }
  242. const cachedAt = baseTime
  243. const deleteAt = determineDeleteAt(baseTime, now, cacheControlDirectives, absoluteStaleAt)
  244. const strippedHeaders = stripNecessaryHeaders(resHeaders, cacheControlDirectives)
  245. /**
  246. * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
  247. */
  248. const value = {
  249. statusCode,
  250. statusMessage,
  251. headers: strippedHeaders,
  252. vary: varyDirectives,
  253. cacheControlDirectives,
  254. cachedAt,
  255. staleAt: absoluteStaleAt,
  256. deleteAt
  257. }
  258. // Not modified, re-use the cached value
  259. // https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-304-not-modified
  260. if (statusCode === 304) {
  261. const handle304 = (cachedValue) => {
  262. if (!cachedValue) {
  263. // Do not create a new cache entry, as a 304 won't have a body - so cannot be cached.
  264. return downstreamOnHeaders()
  265. }
  266. // Re-use the cached value: statuscode, statusmessage, headers and body
  267. value.statusCode = cachedValue.statusCode
  268. value.statusMessage = cachedValue.statusMessage
  269. value.etag = cachedValue.etag
  270. value.vary = varyDirectives ?? cachedValue.vary
  271. value.headers = { ...cachedValue.headers, ...strippedHeaders }
  272. downstreamOnHeaders()
  273. this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
  274. if (!this.#writeStream || !cachedValue?.body) {
  275. return
  276. }
  277. if (typeof cachedValue.body.values === 'function') {
  278. const bodyIterator = cachedValue.body.values()
  279. const streamCachedBody = () => {
  280. for (const chunk of bodyIterator) {
  281. const full = this.#writeStream.write(chunk) === false
  282. this.#handler.onResponseData?.(controller, chunk)
  283. // when stream is full stop writing until we get a 'drain' event
  284. if (full) {
  285. break
  286. }
  287. }
  288. }
  289. this.#writeStream
  290. .on('error', function () {
  291. handler.#writeStream = undefined
  292. handler.#store.delete(handler.#cacheKey)
  293. })
  294. .on('drain', () => {
  295. streamCachedBody()
  296. })
  297. .on('close', function () {
  298. if (handler.#writeStream === this) {
  299. handler.#writeStream = undefined
  300. }
  301. })
  302. streamCachedBody()
  303. } else if (typeof cachedValue.body.on === 'function') {
  304. // Readable stream body (e.g. from async/remote cache stores)
  305. cachedValue.body
  306. .on('data', (chunk) => {
  307. this.#writeStream.write(chunk)
  308. this.#handler.onResponseData?.(controller, chunk)
  309. })
  310. .on('end', () => {
  311. this.#writeStream.end()
  312. })
  313. .on('error', () => {
  314. this.#writeStream = undefined
  315. this.#store.delete(this.#cacheKey)
  316. })
  317. this.#writeStream
  318. .on('error', function () {
  319. handler.#writeStream = undefined
  320. handler.#store.delete(handler.#cacheKey)
  321. })
  322. .on('close', function () {
  323. if (handler.#writeStream === this) {
  324. handler.#writeStream = undefined
  325. }
  326. })
  327. }
  328. }
  329. /**
  330. * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
  331. */
  332. const result = this.#store.get(this.#cacheKey)
  333. if (result && typeof result.then === 'function') {
  334. result.then(handle304)
  335. } else {
  336. handle304(result)
  337. }
  338. } else {
  339. if (typeof resHeaders.etag === 'string' && isEtagUsable(resHeaders.etag)) {
  340. value.etag = resHeaders.etag
  341. }
  342. this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
  343. if (!this.#writeStream) {
  344. return downstreamOnHeaders()
  345. }
  346. this.#writeStream
  347. .on('drain', () => controller.resume())
  348. .on('error', function () {
  349. // TODO (fix): Make error somehow observable?
  350. handler.#writeStream = undefined
  351. // Delete the value in case the cache store is holding onto state from
  352. // the call to createWriteStream
  353. handler.#store.delete(handler.#cacheKey)
  354. })
  355. .on('close', function () {
  356. if (handler.#writeStream === this) {
  357. handler.#writeStream = undefined
  358. }
  359. // TODO (fix): Should we resume even if was paused downstream?
  360. controller.resume()
  361. })
  362. downstreamOnHeaders()
  363. }
  364. }
  365. onResponseData (controller, chunk) {
  366. if (this.#writeStream?.write(chunk) === false) {
  367. controller.pause()
  368. }
  369. this.#handler.onResponseData?.(controller, chunk)
  370. }
  371. onResponseEnd (controller, trailers) {
  372. this.#writeStream?.end()
  373. this.#handler.onResponseEnd?.(controller, trailers)
  374. }
  375. onResponseError (controller, err) {
  376. this.#writeStream?.destroy(err)
  377. this.#writeStream = undefined
  378. this.#handler.onResponseError?.(controller, err)
  379. }
  380. }
  381. /**
  382. * @param {import('../../types/cache-interceptor.d.ts').default.CacheStore} store
  383. * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
  384. */
  385. function deleteCachedValue (store, cacheKey) {
  386. try {
  387. store.delete(cacheKey)?.catch?.(noop)
  388. } catch {
  389. // Fail silently
  390. }
  391. }
  392. function deleteCachedValueIfNotModified (statusCode, store, cacheKey) {
  393. if (statusCode === 304) {
  394. deleteCachedValue(store, cacheKey)
  395. }
  396. }
  397. /**
  398. * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
  399. * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
  400. * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
  401. * @returns {boolean}
  402. */
  403. function revalidationResponseDisallowsCachedReuse (cacheType, resHeaders, cacheControlDirectives) {
  404. return cacheControlDirectives['no-store'] === true ||
  405. (cacheType === 'shared' && (
  406. cacheControlDirectives.private === true ||
  407. Object.hasOwn(resHeaders, 'set-cookie')
  408. )) ||
  409. (resHeaders.vary ? isInvalidOrWildcardVaryHeader(resHeaders.vary) : false)
  410. }
  411. /**
  412. * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
  413. *
  414. * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
  415. * @param {string} method
  416. * @param {number} statusCode
  417. * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
  418. * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
  419. * @param {import('../../types/header.d.ts').IncomingHttpHeaders} [reqHeaders]
  420. */
  421. function canCacheResponse (cacheType, method, statusCode, resHeaders, cacheControlDirectives, reqHeaders) {
  422. if (!arrayIncludes(util.safeHTTPMethods, method)) {
  423. return false
  424. }
  425. // Status code must be final and understood.
  426. if (statusCode < 200 || arrayIncludes(NOT_UNDERSTOOD_STATUS_CODES, statusCode)) {
  427. return false
  428. }
  429. // Responses with neither status codes that are heuristically cacheable, nor "explicit enough" caching
  430. // directives, are not cacheable. "Explicit enough": see https://www.rfc-editor.org/rfc/rfc9111.html#section-3
  431. if (!arrayIncludes(HEURISTICALLY_CACHEABLE_STATUS_CODES, statusCode) && !resHeaders['expires'] &&
  432. !cacheControlDirectives.public &&
  433. cacheControlDirectives['max-age'] === undefined &&
  434. // RFC 9111: a private response directive, if the cache is not shared
  435. !(cacheControlDirectives.private && cacheType === 'private') &&
  436. !(cacheControlDirectives['s-maxage'] !== undefined && cacheType === 'shared')
  437. ) {
  438. return false
  439. }
  440. if (cacheControlDirectives['no-store']) {
  441. return false
  442. }
  443. if (cacheType === 'shared' && (
  444. cacheControlDirectives.private === true ||
  445. Object.hasOwn(resHeaders, 'set-cookie')
  446. )) {
  447. return false
  448. }
  449. // https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5
  450. if (resHeaders.vary && hasVaryStar(resHeaders.vary)) {
  451. return false
  452. }
  453. // https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
  454. if (reqHeaders != null && Object.hasOwn(reqHeaders, 'authorization')) {
  455. if (
  456. !cacheControlDirectives.public &&
  457. !cacheControlDirectives['s-maxage'] &&
  458. !cacheControlDirectives['must-revalidate']
  459. ) {
  460. return false
  461. }
  462. if (typeof reqHeaders.authorization !== 'string') {
  463. return false
  464. }
  465. if (
  466. Array.isArray(cacheControlDirectives['no-cache']) &&
  467. arrayIncludes(cacheControlDirectives['no-cache'], 'authorization')
  468. ) {
  469. return false
  470. }
  471. if (
  472. Array.isArray(cacheControlDirectives['private']) &&
  473. arrayIncludes(cacheControlDirectives['private'], 'authorization')
  474. ) {
  475. return false
  476. }
  477. }
  478. return true
  479. }
  480. /**
  481. * @param {string | string[]} dateHeader
  482. * @returns {Date | null | undefined}
  483. */
  484. function getDate (dateHeader) {
  485. let dateValue = dateHeader
  486. if (Array.isArray(dateValue)) {
  487. if (dateValue.length !== 1) {
  488. return null
  489. }
  490. dateValue = dateValue[0]
  491. }
  492. if (typeof dateValue !== 'string') {
  493. return null
  494. }
  495. return parseHttpDate(dateValue)
  496. }
  497. /**
  498. * @param {string | string[]} ageHeader
  499. * @returns {number | undefined}
  500. */
  501. function getAge (ageHeader) {
  502. let ageValue = ageHeader
  503. if (Array.isArray(ageValue)) {
  504. if (ageValue.length !== 1) {
  505. return MAX_RESPONSE_AGE
  506. }
  507. ageValue = ageValue[0]
  508. }
  509. if (typeof ageValue !== 'string' || !/^[\t ]*[0-9]+[\t ]*$/.test(ageValue)) {
  510. return MAX_RESPONSE_AGE
  511. }
  512. const age = BigInt(ageValue.replace(/^[\t ]+|[\t ]+$/g, ''))
  513. if (age >= BigInt(MAX_RESPONSE_AGE / 1000)) {
  514. return MAX_RESPONSE_AGE
  515. }
  516. return Number(age) * 1000
  517. }
  518. /**
  519. * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
  520. * @param {number} now
  521. * @param {number | undefined} age
  522. * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
  523. * @param {Date | undefined} responseDate
  524. * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
  525. *
  526. * @returns {number | undefined} time that the value is stale at in seconds or undefined if it shouldn't be cached
  527. */
  528. function determineStaleAt (cacheType, now, age, resHeaders, responseDate, cacheControlDirectives) {
  529. if (cacheType === 'shared') {
  530. // Prioritize s-maxage since we're a shared cache
  531. // s-maxage > max-age > Expire
  532. // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3
  533. if (hasInvalidCacheControlDirective(cacheControlDirectives, 's-maxage')) {
  534. return 0
  535. }
  536. const sMaxAge = cacheControlDirectives['s-maxage']
  537. if (sMaxAge !== undefined) {
  538. return sMaxAge * 1000
  539. }
  540. }
  541. if (hasInvalidCacheControlDirective(cacheControlDirectives, 'max-age')) {
  542. return 0
  543. }
  544. const maxAge = cacheControlDirectives['max-age']
  545. if (maxAge !== undefined) {
  546. return maxAge * 1000
  547. }
  548. if (Object.hasOwn(resHeaders, 'expires')) {
  549. // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
  550. if (typeof resHeaders.expires !== 'string') {
  551. return 0
  552. }
  553. const expiresDate = parseHttpDate(resHeaders.expires)
  554. if (!expiresDate) {
  555. return 0
  556. }
  557. if (now >= expiresDate.getTime()) {
  558. return 0
  559. }
  560. if (responseDate) {
  561. if (responseDate >= expiresDate) {
  562. return 0
  563. }
  564. const freshnessLifetime = expiresDate.getTime() - responseDate.getTime()
  565. if (age !== undefined && age >= freshnessLifetime) {
  566. return 0
  567. }
  568. return freshnessLifetime
  569. }
  570. return expiresDate.getTime() - now
  571. }
  572. if (typeof resHeaders['last-modified'] === 'string') {
  573. // https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-heuristic-fresh
  574. const lastModified = parseHttpDate(resHeaders['last-modified'])
  575. if (lastModified) {
  576. if (lastModified.getTime() >= now) {
  577. return undefined
  578. }
  579. const responseAge = now - lastModified.getTime()
  580. return responseAge * 0.1
  581. }
  582. }
  583. if (cacheControlDirectives.immutable) {
  584. // https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
  585. return 31536000000
  586. }
  587. return undefined
  588. }
  589. /**
  590. * @param {number} baseTime
  591. * @param {number} cachedAt
  592. * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
  593. * @param {number} staleAt
  594. */
  595. function determineDeleteAt (baseTime, cachedAt, cacheControlDirectives, staleAt) {
  596. let staleWhileRevalidate = -Infinity
  597. let staleIfError = -Infinity
  598. let immutable = -Infinity
  599. if (cacheControlDirectives['stale-while-revalidate']) {
  600. staleWhileRevalidate = staleAt + (cacheControlDirectives['stale-while-revalidate'] * 1000)
  601. }
  602. if (cacheControlDirectives['stale-if-error']) {
  603. staleIfError = staleAt + (cacheControlDirectives['stale-if-error'] * 1000)
  604. }
  605. if (cacheControlDirectives.immutable && staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
  606. immutable = cachedAt + 31536000000
  607. }
  608. // When no stale directives or immutable flag, add a revalidation buffer
  609. // equal to the freshness lifetime so the entry survives past staleAt long
  610. // enough to be revalidated instead of silently disappearing.
  611. //
  612. // Response Date headers only have second precision, so baseTime can trail the
  613. // actual cache insertion time by up to ~1s. Pad the buffer by that bounded
  614. // skew so short-lived entries do not disappear exactly when they should be
  615. // revalidated.
  616. if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity && immutable === -Infinity) {
  617. const freshnessLifetime = staleAt - baseTime
  618. const datePrecisionPadding = Math.min(Math.max(cachedAt - baseTime, 0), 1000)
  619. return staleAt + freshnessLifetime + datePrecisionPadding
  620. }
  621. return Math.max(staleAt, staleWhileRevalidate, staleIfError, immutable)
  622. }
  623. /**
  624. * Strips headers required to be removed in cached responses
  625. * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
  626. * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
  627. * @returns {Record<string, string | string []>}
  628. */
  629. function stripNecessaryHeaders (resHeaders, cacheControlDirectives) {
  630. const headersToRemove = [
  631. 'connection',
  632. 'proxy-authenticate',
  633. 'proxy-authentication-info',
  634. 'proxy-authorization',
  635. 'proxy-connection',
  636. 'te',
  637. 'transfer-encoding',
  638. 'upgrade',
  639. // We'll add age back when serving it
  640. 'age'
  641. ]
  642. if (resHeaders['connection']) {
  643. appendConnectionHeaderTokens(headersToRemove, resHeaders['connection'])
  644. }
  645. if (Array.isArray(cacheControlDirectives['no-cache'])) {
  646. headersToRemove.push(...cacheControlDirectives['no-cache'])
  647. }
  648. if (Array.isArray(cacheControlDirectives['private'])) {
  649. headersToRemove.push(...cacheControlDirectives['private'])
  650. }
  651. let strippedHeaders
  652. for (const headerName of headersToRemove) {
  653. if (Object.hasOwn(resHeaders, headerName)) {
  654. strippedHeaders ??= { ...resHeaders }
  655. delete strippedHeaders[headerName]
  656. }
  657. }
  658. return strippedHeaders ?? resHeaders
  659. }
  660. module.exports = CacheHandler