cache.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. 'use strict'
  2. const assert = require('node:assert')
  3. const { Readable } = require('node:stream')
  4. const util = require('../core/util')
  5. const CacheHandler = require('../handler/cache-handler')
  6. const MemoryCacheStore = require('../cache/memory-cache-store')
  7. const CacheRevalidationHandler = require('../handler/cache-revalidation-handler')
  8. const { assertCacheStore, assertCacheMethods, makeCacheKey, normaliseHeaders, parseCacheControlHeader } = require('../util/cache.js')
  9. const { AbortError } = require('../core/errors.js')
  10. /**
  11. * @typedef {(options: import('../../types/dispatcher.d.ts').default.DispatchOptions, handler: import('../../types/dispatcher.d.ts').default.DispatchHandler) => void} DispatchFn
  12. */
  13. /**
  14. * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
  15. * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} cacheControlDirectives
  16. * @returns {boolean}
  17. */
  18. function needsRevalidation (result, cacheControlDirectives) {
  19. if (cacheControlDirectives?.['no-cache']) {
  20. // Always revalidate requests with the no-cache request directive
  21. return true
  22. }
  23. if (result.cacheControlDirectives?.['no-cache'] && !Array.isArray(result.cacheControlDirectives['no-cache'])) {
  24. // Always revalidate requests with unqualified no-cache response directive
  25. return true
  26. }
  27. const now = Date.now()
  28. if (now > result.staleAt) {
  29. // Response is stale
  30. if (cacheControlDirectives?.['max-stale']) {
  31. // There's a threshold where we can serve stale responses, let's see if
  32. // we're in it
  33. // https://www.rfc-editor.org/rfc/rfc9111.html#name-max-stale
  34. const gracePeriod = result.staleAt + (cacheControlDirectives['max-stale'] * 1000)
  35. return now > gracePeriod
  36. }
  37. return true
  38. }
  39. if (cacheControlDirectives?.['min-fresh']) {
  40. // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.3
  41. // At this point, staleAt is always > now
  42. const timeLeftTillStale = result.staleAt - now
  43. const threshold = cacheControlDirectives['min-fresh'] * 1000
  44. return timeLeftTillStale <= threshold
  45. }
  46. return false
  47. }
  48. /**
  49. * @param {DispatchFn} dispatch
  50. * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
  51. * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
  52. * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
  53. * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
  54. * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} reqCacheControl
  55. */
  56. function handleUncachedResponse (
  57. dispatch,
  58. globalOpts,
  59. cacheKey,
  60. handler,
  61. opts,
  62. reqCacheControl
  63. ) {
  64. if (reqCacheControl?.['only-if-cached']) {
  65. let aborted = false
  66. try {
  67. if (typeof handler.onConnect === 'function') {
  68. handler.onConnect(() => {
  69. aborted = true
  70. })
  71. if (aborted) {
  72. return
  73. }
  74. }
  75. if (typeof handler.onHeaders === 'function') {
  76. handler.onHeaders(504, [], () => {}, 'Gateway Timeout')
  77. if (aborted) {
  78. return
  79. }
  80. }
  81. if (typeof handler.onComplete === 'function') {
  82. handler.onComplete([])
  83. }
  84. } catch (err) {
  85. if (typeof handler.onError === 'function') {
  86. handler.onError(err)
  87. }
  88. }
  89. return true
  90. }
  91. return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
  92. }
  93. /**
  94. * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
  95. * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
  96. * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
  97. * @param {number} age
  98. * @param {any} context
  99. * @param {boolean} isStale
  100. */
  101. function sendCachedValue (handler, opts, result, age, context, isStale) {
  102. // TODO (perf): Readable.from path can be optimized...
  103. const stream = util.isStream(result.body)
  104. ? result.body
  105. : Readable.from(result.body ?? [])
  106. assert(!stream.destroyed, 'stream should not be destroyed')
  107. assert(!stream.readableDidRead, 'stream should not be readableDidRead')
  108. const controller = {
  109. resume () {
  110. stream.resume()
  111. },
  112. pause () {
  113. stream.pause()
  114. },
  115. get paused () {
  116. return stream.isPaused()
  117. },
  118. get aborted () {
  119. return stream.destroyed
  120. },
  121. get reason () {
  122. return stream.errored
  123. },
  124. abort (reason) {
  125. stream.destroy(reason ?? new AbortError())
  126. }
  127. }
  128. stream
  129. .on('error', function (err) {
  130. if (!this.readableEnded) {
  131. if (typeof handler.onResponseError === 'function') {
  132. handler.onResponseError(controller, err)
  133. } else {
  134. throw err
  135. }
  136. }
  137. })
  138. .on('close', function () {
  139. if (!this.errored) {
  140. handler.onResponseEnd?.(controller, {})
  141. }
  142. })
  143. handler.onRequestStart?.(controller, context)
  144. if (stream.destroyed) {
  145. return
  146. }
  147. // Add the age header
  148. // https://www.rfc-editor.org/rfc/rfc9111.html#name-age
  149. const headers = { ...result.headers, age: String(age) }
  150. if (isStale) {
  151. // Add warning header
  152. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Warning
  153. headers.warning = '110 - "response is stale"'
  154. }
  155. handler.onResponseStart?.(controller, result.statusCode, headers, result.statusMessage)
  156. if (opts.method === 'HEAD') {
  157. stream.destroy()
  158. } else {
  159. stream.on('data', function (chunk) {
  160. handler.onResponseData?.(controller, chunk)
  161. })
  162. }
  163. }
  164. /**
  165. * @param {DispatchFn} dispatch
  166. * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
  167. * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
  168. * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
  169. * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
  170. * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} reqCacheControl
  171. * @param {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined} result
  172. */
  173. function handleResult (
  174. dispatch,
  175. globalOpts,
  176. cacheKey,
  177. handler,
  178. opts,
  179. reqCacheControl,
  180. result
  181. ) {
  182. if (!result) {
  183. return handleUncachedResponse(dispatch, globalOpts, cacheKey, handler, opts, reqCacheControl)
  184. }
  185. const now = Date.now()
  186. if (now > result.deleteAt) {
  187. // Response is expired, cache store shouldn't have given this to us
  188. return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
  189. }
  190. const age = Math.round((now - result.cachedAt) / 1000)
  191. if (reqCacheControl?.['max-age'] && age >= reqCacheControl['max-age']) {
  192. // Response is considered expired for this specific request
  193. // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1
  194. return dispatch(opts, handler)
  195. }
  196. // Check if the response is stale
  197. if (needsRevalidation(result, reqCacheControl)) {
  198. if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) {
  199. // If body is a stream we can't revalidate...
  200. // TODO (fix): This could be less strict...
  201. return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
  202. }
  203. let withinStaleIfErrorThreshold = false
  204. const staleIfErrorExpiry = result.cacheControlDirectives['stale-if-error'] ?? reqCacheControl?.['stale-if-error']
  205. if (staleIfErrorExpiry) {
  206. withinStaleIfErrorThreshold = now < (result.staleAt + (staleIfErrorExpiry * 1000))
  207. }
  208. let headers = {
  209. ...opts.headers,
  210. 'if-modified-since': new Date(result.cachedAt).toUTCString()
  211. }
  212. if (result.etag) {
  213. headers['if-none-match'] = result.etag
  214. }
  215. if (result.vary) {
  216. headers = {
  217. ...headers,
  218. ...result.vary
  219. }
  220. }
  221. // We need to revalidate the response
  222. return dispatch(
  223. {
  224. ...opts,
  225. headers
  226. },
  227. new CacheRevalidationHandler(
  228. (success, context) => {
  229. if (success) {
  230. sendCachedValue(handler, opts, result, age, context, true)
  231. } else if (util.isStream(result.body)) {
  232. result.body.on('error', () => {}).destroy()
  233. }
  234. },
  235. new CacheHandler(globalOpts, cacheKey, handler),
  236. withinStaleIfErrorThreshold
  237. )
  238. )
  239. }
  240. // Dump request body.
  241. if (util.isStream(opts.body)) {
  242. opts.body.on('error', () => {}).destroy()
  243. }
  244. sendCachedValue(handler, opts, result, age, null, false)
  245. }
  246. /**
  247. * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} [opts]
  248. * @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor}
  249. */
  250. module.exports = (opts = {}) => {
  251. const {
  252. store = new MemoryCacheStore(),
  253. methods = ['GET'],
  254. cacheByDefault = undefined,
  255. type = 'shared'
  256. } = opts
  257. if (typeof opts !== 'object' || opts === null) {
  258. throw new TypeError(`expected type of opts to be an Object, got ${opts === null ? 'null' : typeof opts}`)
  259. }
  260. assertCacheStore(store, 'opts.store')
  261. assertCacheMethods(methods, 'opts.methods')
  262. if (typeof cacheByDefault !== 'undefined' && typeof cacheByDefault !== 'number') {
  263. throw new TypeError(`expected opts.cacheByDefault to be number or undefined, got ${typeof cacheByDefault}`)
  264. }
  265. if (typeof type !== 'undefined' && type !== 'shared' && type !== 'private') {
  266. throw new TypeError(`expected opts.type to be shared, private, or undefined, got ${typeof type}`)
  267. }
  268. const globalOpts = {
  269. store,
  270. methods,
  271. cacheByDefault,
  272. type
  273. }
  274. const safeMethodsToNotCache = util.safeHTTPMethods.filter(method => methods.includes(method) === false)
  275. return dispatch => {
  276. return (opts, handler) => {
  277. if (!opts.origin || safeMethodsToNotCache.includes(opts.method)) {
  278. // Not a method we want to cache or we don't have the origin, skip
  279. return dispatch(opts, handler)
  280. }
  281. opts = {
  282. ...opts,
  283. headers: normaliseHeaders(opts)
  284. }
  285. const reqCacheControl = opts.headers?.['cache-control']
  286. ? parseCacheControlHeader(opts.headers['cache-control'])
  287. : undefined
  288. if (reqCacheControl?.['no-store']) {
  289. return dispatch(opts, handler)
  290. }
  291. /**
  292. * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
  293. */
  294. const cacheKey = makeCacheKey(opts)
  295. const result = store.get(cacheKey)
  296. if (result && typeof result.then === 'function') {
  297. result.then(result => {
  298. handleResult(dispatch,
  299. globalOpts,
  300. cacheKey,
  301. handler,
  302. opts,
  303. reqCacheControl,
  304. result
  305. )
  306. })
  307. } else {
  308. handleResult(
  309. dispatch,
  310. globalOpts,
  311. cacheKey,
  312. handler,
  313. opts,
  314. reqCacheControl,
  315. result
  316. )
  317. }
  318. return true
  319. }
  320. }
  321. }