cache-handler.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  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. const MAX_RESPONSE_AGE = 2147483647000
  15. /**
  16. * @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler
  17. *
  18. * @implements {DispatchHandler}
  19. */
  20. class CacheHandler {
  21. /**
  22. * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
  23. */
  24. #cacheKey
  25. /**
  26. * @type {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions['type']}
  27. */
  28. #cacheType
  29. /**
  30. * @type {number | undefined}
  31. */
  32. #cacheByDefault
  33. /**
  34. * @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
  35. */
  36. #store
  37. /**
  38. * @type {import('../../types/dispatcher.d.ts').default.DispatchHandler}
  39. */
  40. #handler
  41. /**
  42. * @type {import('node:stream').Writable | undefined}
  43. */
  44. #writeStream
  45. /**
  46. * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} opts
  47. * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
  48. * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
  49. */
  50. constructor ({ store, type, cacheByDefault }, cacheKey, handler) {
  51. this.#store = store
  52. this.#cacheType = type
  53. this.#cacheByDefault = cacheByDefault
  54. this.#cacheKey = cacheKey
  55. this.#handler = handler
  56. }
  57. onRequestStart (controller, context) {
  58. this.#writeStream?.destroy()
  59. this.#writeStream = undefined
  60. this.#handler.onRequestStart?.(controller, context)
  61. }
  62. onRequestUpgrade (controller, statusCode, headers, socket) {
  63. this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
  64. }
  65. /**
  66. * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
  67. * @param {number} statusCode
  68. * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
  69. * @param {string} statusMessage
  70. */
  71. onResponseStart (
  72. controller,
  73. statusCode,
  74. resHeaders,
  75. statusMessage
  76. ) {
  77. const downstreamOnHeaders = () =>
  78. this.#handler.onResponseStart?.(
  79. controller,
  80. statusCode,
  81. resHeaders,
  82. statusMessage
  83. )
  84. if (
  85. !util.safeHTTPMethods.includes(this.#cacheKey.method) &&
  86. statusCode >= 200 &&
  87. statusCode <= 399
  88. ) {
  89. // Successful response to an unsafe method, delete it from cache
  90. // https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-response
  91. try {
  92. this.#store.delete(this.#cacheKey)?.catch?.(noop)
  93. } catch {
  94. // Fail silently
  95. }
  96. return downstreamOnHeaders()
  97. }
  98. const cacheControlHeader = resHeaders['cache-control']
  99. const heuristicallyCacheable = resHeaders['last-modified'] && HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode)
  100. if (
  101. !cacheControlHeader &&
  102. !resHeaders['expires'] &&
  103. !heuristicallyCacheable &&
  104. !this.#cacheByDefault
  105. ) {
  106. // Don't have anything to tell us this response is cachable and we're not
  107. // caching by default
  108. return downstreamOnHeaders()
  109. }
  110. const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
  111. if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives)) {
  112. return downstreamOnHeaders()
  113. }
  114. const now = Date.now()
  115. const resAge = resHeaders.age ? getAge(resHeaders.age) : undefined
  116. if (resAge && resAge >= MAX_RESPONSE_AGE) {
  117. // Response considered stale
  118. return downstreamOnHeaders()
  119. }
  120. const resDate = typeof resHeaders.date === 'string'
  121. ? parseHttpDate(resHeaders.date)
  122. : undefined
  123. const staleAt =
  124. determineStaleAt(this.#cacheType, now, resAge, resHeaders, resDate, cacheControlDirectives) ??
  125. this.#cacheByDefault
  126. if (staleAt === undefined || (resAge && resAge > staleAt)) {
  127. return downstreamOnHeaders()
  128. }
  129. const baseTime = resDate ? resDate.getTime() : now
  130. const absoluteStaleAt = staleAt + baseTime
  131. if (now >= absoluteStaleAt) {
  132. // Response is already stale
  133. return downstreamOnHeaders()
  134. }
  135. let varyDirectives
  136. if (this.#cacheKey.headers && resHeaders.vary) {
  137. varyDirectives = parseVaryHeader(resHeaders.vary, this.#cacheKey.headers)
  138. if (!varyDirectives) {
  139. // Parse error
  140. return downstreamOnHeaders()
  141. }
  142. }
  143. const deleteAt = determineDeleteAt(baseTime, cacheControlDirectives, absoluteStaleAt)
  144. const strippedHeaders = stripNecessaryHeaders(resHeaders, cacheControlDirectives)
  145. /**
  146. * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
  147. */
  148. const value = {
  149. statusCode,
  150. statusMessage,
  151. headers: strippedHeaders,
  152. vary: varyDirectives,
  153. cacheControlDirectives,
  154. cachedAt: resAge ? now - resAge : now,
  155. staleAt: absoluteStaleAt,
  156. deleteAt
  157. }
  158. if (typeof resHeaders.etag === 'string' && isEtagUsable(resHeaders.etag)) {
  159. value.etag = resHeaders.etag
  160. }
  161. this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
  162. if (!this.#writeStream) {
  163. return downstreamOnHeaders()
  164. }
  165. const handler = this
  166. this.#writeStream
  167. .on('drain', () => controller.resume())
  168. .on('error', function () {
  169. // TODO (fix): Make error somehow observable?
  170. handler.#writeStream = undefined
  171. // Delete the value in case the cache store is holding onto state from
  172. // the call to createWriteStream
  173. handler.#store.delete(handler.#cacheKey)
  174. })
  175. .on('close', function () {
  176. if (handler.#writeStream === this) {
  177. handler.#writeStream = undefined
  178. }
  179. // TODO (fix): Should we resume even if was paused downstream?
  180. controller.resume()
  181. })
  182. return downstreamOnHeaders()
  183. }
  184. onResponseData (controller, chunk) {
  185. if (this.#writeStream?.write(chunk) === false) {
  186. controller.pause()
  187. }
  188. this.#handler.onResponseData?.(controller, chunk)
  189. }
  190. onResponseEnd (controller, trailers) {
  191. this.#writeStream?.end()
  192. this.#handler.onResponseEnd?.(controller, trailers)
  193. }
  194. onResponseError (controller, err) {
  195. this.#writeStream?.destroy(err)
  196. this.#writeStream = undefined
  197. this.#handler.onResponseError?.(controller, err)
  198. }
  199. }
  200. /**
  201. * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
  202. *
  203. * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
  204. * @param {number} statusCode
  205. * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
  206. * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
  207. */
  208. function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives) {
  209. // Allow caching for status codes 200 and 307 (original behavior)
  210. // Also allow caching for other status codes that are heuristically cacheable
  211. // when they have explicit cache directives
  212. if (statusCode !== 200 && statusCode !== 307 && !HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode)) {
  213. return false
  214. }
  215. if (cacheControlDirectives['no-store']) {
  216. return false
  217. }
  218. if (cacheType === 'shared' && cacheControlDirectives.private === true) {
  219. return false
  220. }
  221. // https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5
  222. if (resHeaders.vary?.includes('*')) {
  223. return false
  224. }
  225. // https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
  226. if (resHeaders.authorization) {
  227. if (!cacheControlDirectives.public || typeof resHeaders.authorization !== 'string') {
  228. return false
  229. }
  230. if (
  231. Array.isArray(cacheControlDirectives['no-cache']) &&
  232. cacheControlDirectives['no-cache'].includes('authorization')
  233. ) {
  234. return false
  235. }
  236. if (
  237. Array.isArray(cacheControlDirectives['private']) &&
  238. cacheControlDirectives['private'].includes('authorization')
  239. ) {
  240. return false
  241. }
  242. }
  243. return true
  244. }
  245. /**
  246. * @param {string | string[]} ageHeader
  247. * @returns {number | undefined}
  248. */
  249. function getAge (ageHeader) {
  250. const age = parseInt(Array.isArray(ageHeader) ? ageHeader[0] : ageHeader)
  251. return isNaN(age) ? undefined : age * 1000
  252. }
  253. /**
  254. * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
  255. * @param {number} now
  256. * @param {number | undefined} age
  257. * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
  258. * @param {Date | undefined} responseDate
  259. * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
  260. *
  261. * @returns {number | undefined} time that the value is stale at in seconds or undefined if it shouldn't be cached
  262. */
  263. function determineStaleAt (cacheType, now, age, resHeaders, responseDate, cacheControlDirectives) {
  264. if (cacheType === 'shared') {
  265. // Prioritize s-maxage since we're a shared cache
  266. // s-maxage > max-age > Expire
  267. // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3
  268. const sMaxAge = cacheControlDirectives['s-maxage']
  269. if (sMaxAge !== undefined) {
  270. return sMaxAge > 0 ? sMaxAge * 1000 : undefined
  271. }
  272. }
  273. const maxAge = cacheControlDirectives['max-age']
  274. if (maxAge !== undefined) {
  275. return maxAge > 0 ? maxAge * 1000 : undefined
  276. }
  277. if (typeof resHeaders.expires === 'string') {
  278. // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
  279. const expiresDate = parseHttpDate(resHeaders.expires)
  280. if (expiresDate) {
  281. if (now >= expiresDate.getTime()) {
  282. return undefined
  283. }
  284. if (responseDate) {
  285. if (responseDate >= expiresDate) {
  286. return undefined
  287. }
  288. if (age !== undefined && age > (expiresDate - responseDate)) {
  289. return undefined
  290. }
  291. }
  292. return expiresDate.getTime() - now
  293. }
  294. }
  295. if (typeof resHeaders['last-modified'] === 'string') {
  296. // https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-heuristic-fresh
  297. const lastModified = new Date(resHeaders['last-modified'])
  298. if (isValidDate(lastModified)) {
  299. if (lastModified.getTime() >= now) {
  300. return undefined
  301. }
  302. const responseAge = now - lastModified.getTime()
  303. return responseAge * 0.1
  304. }
  305. }
  306. if (cacheControlDirectives.immutable) {
  307. // https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
  308. return 31536000
  309. }
  310. return undefined
  311. }
  312. /**
  313. * @param {number} now
  314. * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
  315. * @param {number} staleAt
  316. */
  317. function determineDeleteAt (now, cacheControlDirectives, staleAt) {
  318. let staleWhileRevalidate = -Infinity
  319. let staleIfError = -Infinity
  320. let immutable = -Infinity
  321. if (cacheControlDirectives['stale-while-revalidate']) {
  322. staleWhileRevalidate = staleAt + (cacheControlDirectives['stale-while-revalidate'] * 1000)
  323. }
  324. if (cacheControlDirectives['stale-if-error']) {
  325. staleIfError = staleAt + (cacheControlDirectives['stale-if-error'] * 1000)
  326. }
  327. if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
  328. immutable = now + 31536000000
  329. }
  330. return Math.max(staleAt, staleWhileRevalidate, staleIfError, immutable)
  331. }
  332. /**
  333. * Strips headers required to be removed in cached responses
  334. * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
  335. * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
  336. * @returns {Record<string, string | string []>}
  337. */
  338. function stripNecessaryHeaders (resHeaders, cacheControlDirectives) {
  339. const headersToRemove = [
  340. 'connection',
  341. 'proxy-authenticate',
  342. 'proxy-authentication-info',
  343. 'proxy-authorization',
  344. 'proxy-connection',
  345. 'te',
  346. 'transfer-encoding',
  347. 'upgrade',
  348. // We'll add age back when serving it
  349. 'age'
  350. ]
  351. if (resHeaders['connection']) {
  352. if (Array.isArray(resHeaders['connection'])) {
  353. // connection: a
  354. // connection: b
  355. headersToRemove.push(...resHeaders['connection'].map(header => header.trim()))
  356. } else {
  357. // connection: a, b
  358. headersToRemove.push(...resHeaders['connection'].split(',').map(header => header.trim()))
  359. }
  360. }
  361. if (Array.isArray(cacheControlDirectives['no-cache'])) {
  362. headersToRemove.push(...cacheControlDirectives['no-cache'])
  363. }
  364. if (Array.isArray(cacheControlDirectives['private'])) {
  365. headersToRemove.push(...cacheControlDirectives['private'])
  366. }
  367. let strippedHeaders
  368. for (const headerName of headersToRemove) {
  369. if (resHeaders[headerName]) {
  370. strippedHeaders ??= { ...resHeaders }
  371. delete strippedHeaders[headerName]
  372. }
  373. }
  374. return strippedHeaders ?? resHeaders
  375. }
  376. /**
  377. * @param {Date} date
  378. * @returns {boolean}
  379. */
  380. function isValidDate (date) {
  381. return date instanceof Date && Number.isFinite(date.valueOf())
  382. }
  383. module.exports = CacheHandler