cache-handler.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. 'use strict'
  2. const util = require('../core/util')
  3. const {
  4. parseCacheControlHeader,
  5. parseVaryHeader,
  6. isEtagUsable
  7. } = require('../util/cache')
  8. const { parseHttpDate } = require('../util/date.js')
  9. function noop () {}
  10. // Status codes that we can use some heuristics on to cache
  11. const HEURISTICALLY_CACHEABLE_STATUS_CODES = [
  12. 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501
  13. ]
  14. // Status codes which semantic is not handled by the cache
  15. // https://datatracker.ietf.org/doc/html/rfc9111#section-3
  16. // This list should not grow beyond 206 unless the RFC is updated
  17. // by a newer one including more. Please introduce another list if
  18. // implementing caching of responses with the 'must-understand' directive.
  19. const NOT_UNDERSTOOD_STATUS_CODES = [
  20. 206
  21. ]
  22. const MAX_RESPONSE_AGE = 2147483647000
  23. /**
  24. * @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler
  25. *
  26. * @implements {DispatchHandler}
  27. */
  28. class CacheHandler {
  29. /**
  30. * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
  31. */
  32. #cacheKey
  33. /**
  34. * @type {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions['type']}
  35. */
  36. #cacheType
  37. /**
  38. * @type {number | undefined}
  39. */
  40. #cacheByDefault
  41. /**
  42. * @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
  43. */
  44. #store
  45. /**
  46. * @type {import('../../types/dispatcher.d.ts').default.DispatchHandler}
  47. */
  48. #handler
  49. /**
  50. * @type {import('node:stream').Writable | undefined}
  51. */
  52. #writeStream
  53. /**
  54. * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} opts
  55. * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
  56. * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
  57. */
  58. constructor ({ store, type, cacheByDefault }, cacheKey, handler) {
  59. this.#store = store
  60. this.#cacheType = type
  61. this.#cacheByDefault = cacheByDefault
  62. this.#cacheKey = cacheKey
  63. this.#handler = handler
  64. }
  65. onRequestStart (controller, context) {
  66. this.#writeStream?.destroy()
  67. this.#writeStream = undefined
  68. this.#handler.onRequestStart?.(controller, context)
  69. }
  70. onRequestUpgrade (controller, statusCode, headers, socket) {
  71. this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
  72. }
  73. /**
  74. * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
  75. * @param {number} statusCode
  76. * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
  77. * @param {string} statusMessage
  78. */
  79. onResponseStart (
  80. controller,
  81. statusCode,
  82. resHeaders,
  83. statusMessage
  84. ) {
  85. const downstreamOnHeaders = () =>
  86. this.#handler.onResponseStart?.(
  87. controller,
  88. statusCode,
  89. resHeaders,
  90. statusMessage
  91. )
  92. const handler = this
  93. if (
  94. !util.safeHTTPMethods.includes(this.#cacheKey.method) &&
  95. statusCode >= 200 &&
  96. statusCode <= 399
  97. ) {
  98. // Successful response to an unsafe method, delete it from cache
  99. // https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-response
  100. try {
  101. this.#store.delete(this.#cacheKey)?.catch?.(noop)
  102. } catch {
  103. // Fail silently
  104. }
  105. return downstreamOnHeaders()
  106. }
  107. const cacheControlHeader = resHeaders['cache-control']
  108. const heuristicallyCacheable = resHeaders['last-modified'] && HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode)
  109. if (
  110. !cacheControlHeader &&
  111. !resHeaders['expires'] &&
  112. !heuristicallyCacheable &&
  113. !this.#cacheByDefault
  114. ) {
  115. // Don't have anything to tell us this response is cachable and we're not
  116. // caching by default
  117. return downstreamOnHeaders()
  118. }
  119. const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
  120. if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives)) {
  121. return downstreamOnHeaders()
  122. }
  123. const now = Date.now()
  124. const resAge = resHeaders.age ? getAge(resHeaders.age) : undefined
  125. if (resAge && resAge >= MAX_RESPONSE_AGE) {
  126. // Response considered stale
  127. return downstreamOnHeaders()
  128. }
  129. const resDate = typeof resHeaders.date === 'string'
  130. ? parseHttpDate(resHeaders.date)
  131. : undefined
  132. const staleAt =
  133. determineStaleAt(this.#cacheType, now, resAge, resHeaders, resDate, cacheControlDirectives) ??
  134. this.#cacheByDefault
  135. if (staleAt === undefined || (resAge && resAge > staleAt)) {
  136. return downstreamOnHeaders()
  137. }
  138. const baseTime = resDate ? resDate.getTime() : now
  139. const absoluteStaleAt = staleAt + baseTime
  140. if (now >= absoluteStaleAt) {
  141. // Response is already stale
  142. return downstreamOnHeaders()
  143. }
  144. let varyDirectives
  145. if (this.#cacheKey.headers && resHeaders.vary) {
  146. varyDirectives = parseVaryHeader(resHeaders.vary, this.#cacheKey.headers)
  147. if (!varyDirectives) {
  148. // Parse error
  149. return downstreamOnHeaders()
  150. }
  151. }
  152. const deleteAt = determineDeleteAt(baseTime, cacheControlDirectives, absoluteStaleAt)
  153. const strippedHeaders = stripNecessaryHeaders(resHeaders, cacheControlDirectives)
  154. /**
  155. * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
  156. */
  157. const value = {
  158. statusCode,
  159. statusMessage,
  160. headers: strippedHeaders,
  161. vary: varyDirectives,
  162. cacheControlDirectives,
  163. cachedAt: resAge ? now - resAge : now,
  164. staleAt: absoluteStaleAt,
  165. deleteAt
  166. }
  167. // Not modified, re-use the cached value
  168. // https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-304-not-modified
  169. if (statusCode === 304) {
  170. const handle304 = (cachedValue) => {
  171. if (!cachedValue) {
  172. // Do not create a new cache entry, as a 304 won't have a body - so cannot be cached.
  173. return downstreamOnHeaders()
  174. }
  175. // Re-use the cached value: statuscode, statusmessage, headers and body
  176. value.statusCode = cachedValue.statusCode
  177. value.statusMessage = cachedValue.statusMessage
  178. value.etag = cachedValue.etag
  179. value.headers = { ...cachedValue.headers, ...strippedHeaders }
  180. downstreamOnHeaders()
  181. this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
  182. if (!this.#writeStream || !cachedValue?.body) {
  183. return
  184. }
  185. if (typeof cachedValue.body.values === 'function') {
  186. const bodyIterator = cachedValue.body.values()
  187. const streamCachedBody = () => {
  188. for (const chunk of bodyIterator) {
  189. const full = this.#writeStream.write(chunk) === false
  190. this.#handler.onResponseData?.(controller, chunk)
  191. // when stream is full stop writing until we get a 'drain' event
  192. if (full) {
  193. break
  194. }
  195. }
  196. }
  197. this.#writeStream
  198. .on('error', function () {
  199. handler.#writeStream = undefined
  200. handler.#store.delete(handler.#cacheKey)
  201. })
  202. .on('drain', () => {
  203. streamCachedBody()
  204. })
  205. .on('close', function () {
  206. if (handler.#writeStream === this) {
  207. handler.#writeStream = undefined
  208. }
  209. })
  210. streamCachedBody()
  211. } else if (typeof cachedValue.body.on === 'function') {
  212. // Readable stream body (e.g. from async/remote cache stores)
  213. cachedValue.body
  214. .on('data', (chunk) => {
  215. this.#writeStream.write(chunk)
  216. this.#handler.onResponseData?.(controller, chunk)
  217. })
  218. .on('end', () => {
  219. this.#writeStream.end()
  220. })
  221. .on('error', () => {
  222. this.#writeStream = undefined
  223. this.#store.delete(this.#cacheKey)
  224. })
  225. this.#writeStream
  226. .on('error', function () {
  227. handler.#writeStream = undefined
  228. handler.#store.delete(handler.#cacheKey)
  229. })
  230. .on('close', function () {
  231. if (handler.#writeStream === this) {
  232. handler.#writeStream = undefined
  233. }
  234. })
  235. }
  236. }
  237. /**
  238. * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
  239. */
  240. const result = this.#store.get(this.#cacheKey)
  241. if (result && typeof result.then === 'function') {
  242. result.then(handle304)
  243. } else {
  244. handle304(result)
  245. }
  246. } else {
  247. if (typeof resHeaders.etag === 'string' && isEtagUsable(resHeaders.etag)) {
  248. value.etag = resHeaders.etag
  249. }
  250. this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
  251. if (!this.#writeStream) {
  252. return downstreamOnHeaders()
  253. }
  254. this.#writeStream
  255. .on('drain', () => controller.resume())
  256. .on('error', function () {
  257. // TODO (fix): Make error somehow observable?
  258. handler.#writeStream = undefined
  259. // Delete the value in case the cache store is holding onto state from
  260. // the call to createWriteStream
  261. handler.#store.delete(handler.#cacheKey)
  262. })
  263. .on('close', function () {
  264. if (handler.#writeStream === this) {
  265. handler.#writeStream = undefined
  266. }
  267. // TODO (fix): Should we resume even if was paused downstream?
  268. controller.resume()
  269. })
  270. downstreamOnHeaders()
  271. }
  272. }
  273. onResponseData (controller, chunk) {
  274. if (this.#writeStream?.write(chunk) === false) {
  275. controller.pause()
  276. }
  277. this.#handler.onResponseData?.(controller, chunk)
  278. }
  279. onResponseEnd (controller, trailers) {
  280. this.#writeStream?.end()
  281. this.#handler.onResponseEnd?.(controller, trailers)
  282. }
  283. onResponseError (controller, err) {
  284. this.#writeStream?.destroy(err)
  285. this.#writeStream = undefined
  286. this.#handler.onResponseError?.(controller, err)
  287. }
  288. }
  289. /**
  290. * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
  291. *
  292. * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
  293. * @param {number} statusCode
  294. * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
  295. * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
  296. */
  297. function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives) {
  298. // Status code must be final and understood.
  299. if (statusCode < 200 || NOT_UNDERSTOOD_STATUS_CODES.includes(statusCode)) {
  300. return false
  301. }
  302. // Responses with neither status codes that are heuristically cacheable, nor "explicit enough" caching
  303. // directives, are not cacheable. "Explicit enough": see https://www.rfc-editor.org/rfc/rfc9111.html#section-3
  304. if (!HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode) && !resHeaders['expires'] &&
  305. !cacheControlDirectives.public &&
  306. cacheControlDirectives['max-age'] === undefined &&
  307. // RFC 9111: a private response directive, if the cache is not shared
  308. !(cacheControlDirectives.private && cacheType === 'private') &&
  309. !(cacheControlDirectives['s-maxage'] !== undefined && cacheType === 'shared')
  310. ) {
  311. return false
  312. }
  313. if (cacheControlDirectives['no-store']) {
  314. return false
  315. }
  316. if (cacheType === 'shared' && cacheControlDirectives.private === true) {
  317. return false
  318. }
  319. // https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5
  320. if (resHeaders.vary?.includes('*')) {
  321. return false
  322. }
  323. // https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
  324. if (resHeaders.authorization) {
  325. if (!cacheControlDirectives.public || typeof resHeaders.authorization !== 'string') {
  326. return false
  327. }
  328. if (
  329. Array.isArray(cacheControlDirectives['no-cache']) &&
  330. cacheControlDirectives['no-cache'].includes('authorization')
  331. ) {
  332. return false
  333. }
  334. if (
  335. Array.isArray(cacheControlDirectives['private']) &&
  336. cacheControlDirectives['private'].includes('authorization')
  337. ) {
  338. return false
  339. }
  340. }
  341. return true
  342. }
  343. /**
  344. * @param {string | string[]} ageHeader
  345. * @returns {number | undefined}
  346. */
  347. function getAge (ageHeader) {
  348. const age = parseInt(Array.isArray(ageHeader) ? ageHeader[0] : ageHeader)
  349. return isNaN(age) ? undefined : age * 1000
  350. }
  351. /**
  352. * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
  353. * @param {number} now
  354. * @param {number | undefined} age
  355. * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
  356. * @param {Date | undefined} responseDate
  357. * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
  358. *
  359. * @returns {number | undefined} time that the value is stale at in seconds or undefined if it shouldn't be cached
  360. */
  361. function determineStaleAt (cacheType, now, age, resHeaders, responseDate, cacheControlDirectives) {
  362. if (cacheType === 'shared') {
  363. // Prioritize s-maxage since we're a shared cache
  364. // s-maxage > max-age > Expire
  365. // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3
  366. const sMaxAge = cacheControlDirectives['s-maxage']
  367. if (sMaxAge !== undefined) {
  368. return sMaxAge > 0 ? sMaxAge * 1000 : undefined
  369. }
  370. }
  371. const maxAge = cacheControlDirectives['max-age']
  372. if (maxAge !== undefined) {
  373. return maxAge > 0 ? maxAge * 1000 : undefined
  374. }
  375. if (typeof resHeaders.expires === 'string') {
  376. // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
  377. const expiresDate = parseHttpDate(resHeaders.expires)
  378. if (expiresDate) {
  379. if (now >= expiresDate.getTime()) {
  380. return undefined
  381. }
  382. if (responseDate) {
  383. if (responseDate >= expiresDate) {
  384. return undefined
  385. }
  386. if (age !== undefined && age > (expiresDate - responseDate)) {
  387. return undefined
  388. }
  389. }
  390. return expiresDate.getTime() - now
  391. }
  392. }
  393. if (typeof resHeaders['last-modified'] === 'string') {
  394. // https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-heuristic-fresh
  395. const lastModified = new Date(resHeaders['last-modified'])
  396. if (isValidDate(lastModified)) {
  397. if (lastModified.getTime() >= now) {
  398. return undefined
  399. }
  400. const responseAge = now - lastModified.getTime()
  401. return responseAge * 0.1
  402. }
  403. }
  404. if (cacheControlDirectives.immutable) {
  405. // https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
  406. return 31536000
  407. }
  408. return undefined
  409. }
  410. /**
  411. * @param {number} now
  412. * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
  413. * @param {number} staleAt
  414. */
  415. function determineDeleteAt (now, cacheControlDirectives, staleAt) {
  416. let staleWhileRevalidate = -Infinity
  417. let staleIfError = -Infinity
  418. let immutable = -Infinity
  419. if (cacheControlDirectives['stale-while-revalidate']) {
  420. staleWhileRevalidate = staleAt + (cacheControlDirectives['stale-while-revalidate'] * 1000)
  421. }
  422. if (cacheControlDirectives['stale-if-error']) {
  423. staleIfError = staleAt + (cacheControlDirectives['stale-if-error'] * 1000)
  424. }
  425. if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
  426. immutable = now + 31536000000
  427. }
  428. return Math.max(staleAt, staleWhileRevalidate, staleIfError, immutable)
  429. }
  430. /**
  431. * Strips headers required to be removed in cached responses
  432. * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
  433. * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
  434. * @returns {Record<string, string | string []>}
  435. */
  436. function stripNecessaryHeaders (resHeaders, cacheControlDirectives) {
  437. const headersToRemove = [
  438. 'connection',
  439. 'proxy-authenticate',
  440. 'proxy-authentication-info',
  441. 'proxy-authorization',
  442. 'proxy-connection',
  443. 'te',
  444. 'transfer-encoding',
  445. 'upgrade',
  446. // We'll add age back when serving it
  447. 'age'
  448. ]
  449. if (resHeaders['connection']) {
  450. if (Array.isArray(resHeaders['connection'])) {
  451. // connection: a
  452. // connection: b
  453. headersToRemove.push(...resHeaders['connection'].map(header => header.trim()))
  454. } else {
  455. // connection: a, b
  456. headersToRemove.push(...resHeaders['connection'].split(',').map(header => header.trim()))
  457. }
  458. }
  459. if (Array.isArray(cacheControlDirectives['no-cache'])) {
  460. headersToRemove.push(...cacheControlDirectives['no-cache'])
  461. }
  462. if (Array.isArray(cacheControlDirectives['private'])) {
  463. headersToRemove.push(...cacheControlDirectives['private'])
  464. }
  465. let strippedHeaders
  466. for (const headerName of headersToRemove) {
  467. if (resHeaders[headerName]) {
  468. strippedHeaders ??= { ...resHeaders }
  469. delete strippedHeaders[headerName]
  470. }
  471. }
  472. return strippedHeaders ?? resHeaders
  473. }
  474. /**
  475. * @param {Date} date
  476. * @returns {boolean}
  477. */
  478. function isValidDate (date) {
  479. return date instanceof Date && Number.isFinite(date.valueOf())
  480. }
  481. module.exports = CacheHandler