| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776 |
- 'use strict'
- const util = require('../core/util')
- const {
- parseCacheControlHeader,
- hasInvalidCacheControlDirective,
- parseVaryHeader,
- hasVaryStar,
- isInvalidOrWildcardVaryHeader,
- isEtagUsable
- } = require('../util/cache')
- const { parseHttpDate } = require('../util/date.js')
- function noop () {}
- // Status codes that we can use some heuristics on to cache
- const HEURISTICALLY_CACHEABLE_STATUS_CODES = [
- 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501
- ]
- // Status codes which semantic is not handled by the cache
- // https://datatracker.ietf.org/doc/html/rfc9111#section-3
- // This list should not grow beyond 206 unless the RFC is updated
- // by a newer one including more. Please introduce another list if
- // implementing caching of responses with the 'must-understand' directive.
- const NOT_UNDERSTOOD_STATUS_CODES = [
- 206
- ]
- const MAX_RESPONSE_AGE = 2147483647000
- function trimOWS (value) {
- return value.replace(/^[\t ]+|[\t ]+$/g, '')
- }
- function arrayIncludes (array, value) {
- for (let i = 0; i < array.length; i++) {
- if (array[i] === value) {
- return true
- }
- }
- return false
- }
- function appendConnectionHeaderTokens (headersToRemove, connectionHeader) {
- const values = Array.isArray(connectionHeader) ? connectionHeader : [connectionHeader]
- for (let i = 0; i < values.length; i++) {
- const tokens = values[i].split(',')
- for (let j = 0; j < tokens.length; j++) {
- headersToRemove.push(trimOWS(tokens[j]).toLowerCase())
- }
- }
- }
- function getSameOriginPath (cacheKey, location) {
- if (typeof location !== 'string') {
- return undefined
- }
- let originUrl
- let requestUrl
- let locationUrl
- try {
- originUrl = new URL(cacheKey.origin)
- requestUrl = new URL(cacheKey.path, originUrl)
- locationUrl = new URL(location, requestUrl)
- } catch {
- return undefined
- }
- if (locationUrl.origin !== originUrl.origin) {
- return undefined
- }
- return locationUrl.pathname + locationUrl.search
- }
- function deleteCachedUri (store, cacheKey, path) {
- deleteCachedValue(store, {
- ...cacheKey,
- path
- })
- for (let i = 0; i < util.safeHTTPMethods.length; i++) {
- const method = util.safeHTTPMethods[i]
- if (method !== cacheKey.method) {
- deleteCachedValue(store, {
- ...cacheKey,
- method,
- path
- })
- }
- }
- }
- function deleteLocationTargets (store, cacheKey, headerValue) {
- if (headerValue === undefined) {
- return
- }
- const values = Array.isArray(headerValue) ? headerValue : [headerValue]
- for (let i = 0; i < values.length; i++) {
- const path = getSameOriginPath(cacheKey, values[i])
- if (path !== undefined) {
- deleteCachedUri(store, cacheKey, path)
- }
- }
- }
- function invalidateUnsafeRequest (store, cacheKey, resHeaders) {
- deleteCachedUri(store, cacheKey, cacheKey.path)
- deleteLocationTargets(store, cacheKey, resHeaders.location)
- deleteLocationTargets(store, cacheKey, resHeaders['content-location'])
- }
- /**
- * @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler
- *
- * @implements {DispatchHandler}
- */
- class CacheHandler {
- /**
- * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
- */
- #cacheKey
- /**
- * @type {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions['type']}
- */
- #cacheType
- /**
- * @type {number | undefined}
- */
- #cacheByDefault
- /**
- * @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
- */
- #store
- /**
- * @type {import('../../types/dispatcher.d.ts').default.DispatchHandler}
- */
- #handler
- /**
- * @type {import('node:stream').Writable | undefined}
- */
- #writeStream
- /**
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} opts
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
- * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
- */
- constructor ({ store, type, cacheByDefault }, cacheKey, handler) {
- this.#store = store
- this.#cacheType = type
- this.#cacheByDefault = cacheByDefault
- this.#cacheKey = cacheKey
- this.#handler = handler
- }
- onRequestStart (controller, context) {
- this.#writeStream?.destroy()
- this.#writeStream = undefined
- this.#handler.onRequestStart?.(controller, context)
- }
- onRequestUpgrade (controller, statusCode, headers, socket) {
- this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
- }
- /**
- * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
- * @param {number} statusCode
- * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
- * @param {string} statusMessage
- */
- onResponseStart (
- controller,
- statusCode,
- resHeaders,
- statusMessage
- ) {
- const downstreamOnHeaders = () =>
- this.#handler.onResponseStart?.(
- controller,
- statusCode,
- resHeaders,
- statusMessage
- )
- const handler = this
- if (
- !arrayIncludes(util.safeHTTPMethods, this.#cacheKey.method) &&
- statusCode >= 200 &&
- statusCode <= 399
- ) {
- // Successful response to an unsafe method, delete it from cache
- // https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-response
- invalidateUnsafeRequest(this.#store, this.#cacheKey, resHeaders)
- return downstreamOnHeaders()
- }
- const cacheControlHeader = resHeaders['cache-control']
- const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
- if (revalidationResponseDisallowsCachedReuse(this.#cacheType, resHeaders, cacheControlDirectives)) {
- deleteCachedValue(this.#store, this.#cacheKey)
- return downstreamOnHeaders()
- }
- const heuristicallyCacheable = resHeaders['last-modified'] && arrayIncludes(HEURISTICALLY_CACHEABLE_STATUS_CODES, statusCode)
- if (
- !cacheControlHeader &&
- !resHeaders['expires'] &&
- !heuristicallyCacheable &&
- !this.#cacheByDefault
- ) {
- if (statusCode === 304 && resHeaders.vary && isInvalidOrWildcardVaryHeader(resHeaders.vary)) {
- deleteCachedValue(this.#store, this.#cacheKey)
- }
- // Don't have anything to tell us this response is cachable and we're not
- // caching by default
- return downstreamOnHeaders()
- }
- if (!canCacheResponse(this.#cacheType, this.#cacheKey.method, statusCode, resHeaders, cacheControlDirectives, this.#cacheKey.headers)) {
- if (statusCode === 304 && (cacheControlHeader || revalidationResponseDisallowsCachedReuse(this.#cacheType, resHeaders, cacheControlDirectives))) {
- deleteCachedValue(this.#store, this.#cacheKey)
- }
- return downstreamOnHeaders()
- }
- const now = Date.now()
- const resAge = Object.hasOwn(resHeaders, 'age') ? getAge(resHeaders.age) : undefined
- if (resAge !== undefined && resAge >= MAX_RESPONSE_AGE) {
- // Response considered stale
- deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey)
- return downstreamOnHeaders()
- }
- const resDate = Object.hasOwn(resHeaders, 'date') ? getDate(resHeaders.date) : undefined
- if (resDate === null) {
- deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey)
- return downstreamOnHeaders()
- }
- const apparentAge = resDate ? Math.max(0, now - resDate.getTime()) : 0
- const currentAge = Math.max(apparentAge, resAge ?? 0)
- const staleAt =
- determineStaleAt(this.#cacheType, now, resAge, resHeaders, resDate, cacheControlDirectives) ??
- this.#cacheByDefault
- if (staleAt === undefined || currentAge >= staleAt) {
- if (cacheControlHeader || staleAt !== undefined) {
- deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey)
- }
- return downstreamOnHeaders()
- }
- const baseTime = now - currentAge
- const absoluteStaleAt = staleAt + baseTime
- if (now >= absoluteStaleAt) {
- // Response is already stale
- deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey)
- return downstreamOnHeaders()
- }
- let varyDirectives
- if (this.#cacheKey.headers && resHeaders.vary) {
- varyDirectives = parseVaryHeader(resHeaders.vary, this.#cacheKey.headers)
- if (!varyDirectives) {
- // Parse error
- return downstreamOnHeaders()
- }
- }
- const cachedAt = baseTime
- const deleteAt = determineDeleteAt(baseTime, now, cacheControlDirectives, absoluteStaleAt)
- const strippedHeaders = stripNecessaryHeaders(resHeaders, cacheControlDirectives)
- /**
- * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
- */
- const value = {
- statusCode,
- statusMessage,
- headers: strippedHeaders,
- vary: varyDirectives,
- cacheControlDirectives,
- cachedAt,
- staleAt: absoluteStaleAt,
- deleteAt
- }
- // Not modified, re-use the cached value
- // https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-304-not-modified
- if (statusCode === 304) {
- const handle304 = (cachedValue) => {
- if (!cachedValue) {
- // Do not create a new cache entry, as a 304 won't have a body - so cannot be cached.
- return downstreamOnHeaders()
- }
- // Re-use the cached value: statuscode, statusmessage, headers and body
- value.statusCode = cachedValue.statusCode
- value.statusMessage = cachedValue.statusMessage
- value.etag = cachedValue.etag
- value.vary = varyDirectives ?? cachedValue.vary
- value.headers = { ...cachedValue.headers, ...strippedHeaders }
- downstreamOnHeaders()
- this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
- if (!this.#writeStream || !cachedValue?.body) {
- return
- }
- if (typeof cachedValue.body.values === 'function') {
- const bodyIterator = cachedValue.body.values()
- const streamCachedBody = () => {
- for (const chunk of bodyIterator) {
- const full = this.#writeStream.write(chunk) === false
- this.#handler.onResponseData?.(controller, chunk)
- // when stream is full stop writing until we get a 'drain' event
- if (full) {
- break
- }
- }
- }
- this.#writeStream
- .on('error', function () {
- handler.#writeStream = undefined
- handler.#store.delete(handler.#cacheKey)
- })
- .on('drain', () => {
- streamCachedBody()
- })
- .on('close', function () {
- if (handler.#writeStream === this) {
- handler.#writeStream = undefined
- }
- })
- streamCachedBody()
- } else if (typeof cachedValue.body.on === 'function') {
- // Readable stream body (e.g. from async/remote cache stores)
- cachedValue.body
- .on('data', (chunk) => {
- this.#writeStream.write(chunk)
- this.#handler.onResponseData?.(controller, chunk)
- })
- .on('end', () => {
- this.#writeStream.end()
- })
- .on('error', () => {
- this.#writeStream = undefined
- this.#store.delete(this.#cacheKey)
- })
- this.#writeStream
- .on('error', function () {
- handler.#writeStream = undefined
- handler.#store.delete(handler.#cacheKey)
- })
- .on('close', function () {
- if (handler.#writeStream === this) {
- handler.#writeStream = undefined
- }
- })
- }
- }
- /**
- * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
- */
- const result = this.#store.get(this.#cacheKey)
- if (result && typeof result.then === 'function') {
- result.then(handle304)
- } else {
- handle304(result)
- }
- } else {
- if (typeof resHeaders.etag === 'string' && isEtagUsable(resHeaders.etag)) {
- value.etag = resHeaders.etag
- }
- this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
- if (!this.#writeStream) {
- return downstreamOnHeaders()
- }
- this.#writeStream
- .on('drain', () => controller.resume())
- .on('error', function () {
- // TODO (fix): Make error somehow observable?
- handler.#writeStream = undefined
- // Delete the value in case the cache store is holding onto state from
- // the call to createWriteStream
- handler.#store.delete(handler.#cacheKey)
- })
- .on('close', function () {
- if (handler.#writeStream === this) {
- handler.#writeStream = undefined
- }
- // TODO (fix): Should we resume even if was paused downstream?
- controller.resume()
- })
- downstreamOnHeaders()
- }
- }
- onResponseData (controller, chunk) {
- if (this.#writeStream?.write(chunk) === false) {
- controller.pause()
- }
- this.#handler.onResponseData?.(controller, chunk)
- }
- onResponseEnd (controller, trailers) {
- this.#writeStream?.end()
- this.#handler.onResponseEnd?.(controller, trailers)
- }
- onResponseError (controller, err) {
- this.#writeStream?.destroy(err)
- this.#writeStream = undefined
- this.#handler.onResponseError?.(controller, err)
- }
- }
- /**
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheStore} store
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
- */
- function deleteCachedValue (store, cacheKey) {
- try {
- store.delete(cacheKey)?.catch?.(noop)
- } catch {
- // Fail silently
- }
- }
- function deleteCachedValueIfNotModified (statusCode, store, cacheKey) {
- if (statusCode === 304) {
- deleteCachedValue(store, cacheKey)
- }
- }
- /**
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
- * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
- * @returns {boolean}
- */
- function revalidationResponseDisallowsCachedReuse (cacheType, resHeaders, cacheControlDirectives) {
- return cacheControlDirectives['no-store'] === true ||
- (cacheType === 'shared' && (
- cacheControlDirectives.private === true ||
- Object.hasOwn(resHeaders, 'set-cookie')
- )) ||
- (resHeaders.vary ? isInvalidOrWildcardVaryHeader(resHeaders.vary) : false)
- }
- /**
- * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
- *
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
- * @param {string} method
- * @param {number} statusCode
- * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
- * @param {import('../../types/header.d.ts').IncomingHttpHeaders} [reqHeaders]
- */
- function canCacheResponse (cacheType, method, statusCode, resHeaders, cacheControlDirectives, reqHeaders) {
- if (!arrayIncludes(util.safeHTTPMethods, method)) {
- return false
- }
- // Status code must be final and understood.
- if (statusCode < 200 || arrayIncludes(NOT_UNDERSTOOD_STATUS_CODES, statusCode)) {
- return false
- }
- // Responses with neither status codes that are heuristically cacheable, nor "explicit enough" caching
- // directives, are not cacheable. "Explicit enough": see https://www.rfc-editor.org/rfc/rfc9111.html#section-3
- if (!arrayIncludes(HEURISTICALLY_CACHEABLE_STATUS_CODES, statusCode) && !resHeaders['expires'] &&
- !cacheControlDirectives.public &&
- cacheControlDirectives['max-age'] === undefined &&
- // RFC 9111: a private response directive, if the cache is not shared
- !(cacheControlDirectives.private && cacheType === 'private') &&
- !(cacheControlDirectives['s-maxage'] !== undefined && cacheType === 'shared')
- ) {
- return false
- }
- if (cacheControlDirectives['no-store']) {
- return false
- }
- if (cacheType === 'shared' && (
- cacheControlDirectives.private === true ||
- Object.hasOwn(resHeaders, 'set-cookie')
- )) {
- return false
- }
- // https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5
- if (resHeaders.vary && hasVaryStar(resHeaders.vary)) {
- return false
- }
- // https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
- if (reqHeaders != null && Object.hasOwn(reqHeaders, 'authorization')) {
- if (
- !cacheControlDirectives.public &&
- !cacheControlDirectives['s-maxage'] &&
- !cacheControlDirectives['must-revalidate']
- ) {
- return false
- }
- if (typeof reqHeaders.authorization !== 'string') {
- return false
- }
- if (
- Array.isArray(cacheControlDirectives['no-cache']) &&
- arrayIncludes(cacheControlDirectives['no-cache'], 'authorization')
- ) {
- return false
- }
- if (
- Array.isArray(cacheControlDirectives['private']) &&
- arrayIncludes(cacheControlDirectives['private'], 'authorization')
- ) {
- return false
- }
- }
- return true
- }
- /**
- * @param {string | string[]} dateHeader
- * @returns {Date | null | undefined}
- */
- function getDate (dateHeader) {
- let dateValue = dateHeader
- if (Array.isArray(dateValue)) {
- if (dateValue.length !== 1) {
- return null
- }
- dateValue = dateValue[0]
- }
- if (typeof dateValue !== 'string') {
- return null
- }
- return parseHttpDate(dateValue)
- }
- /**
- * @param {string | string[]} ageHeader
- * @returns {number | undefined}
- */
- function getAge (ageHeader) {
- let ageValue = ageHeader
- if (Array.isArray(ageValue)) {
- if (ageValue.length !== 1) {
- return MAX_RESPONSE_AGE
- }
- ageValue = ageValue[0]
- }
- if (typeof ageValue !== 'string' || !/^[\t ]*[0-9]+[\t ]*$/.test(ageValue)) {
- return MAX_RESPONSE_AGE
- }
- const age = BigInt(ageValue.replace(/^[\t ]+|[\t ]+$/g, ''))
- if (age >= BigInt(MAX_RESPONSE_AGE / 1000)) {
- return MAX_RESPONSE_AGE
- }
- return Number(age) * 1000
- }
- /**
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
- * @param {number} now
- * @param {number | undefined} age
- * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
- * @param {Date | undefined} responseDate
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
- *
- * @returns {number | undefined} time that the value is stale at in seconds or undefined if it shouldn't be cached
- */
- function determineStaleAt (cacheType, now, age, resHeaders, responseDate, cacheControlDirectives) {
- if (cacheType === 'shared') {
- // Prioritize s-maxage since we're a shared cache
- // s-maxage > max-age > Expire
- // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3
- if (hasInvalidCacheControlDirective(cacheControlDirectives, 's-maxage')) {
- return 0
- }
- const sMaxAge = cacheControlDirectives['s-maxage']
- if (sMaxAge !== undefined) {
- return sMaxAge * 1000
- }
- }
- if (hasInvalidCacheControlDirective(cacheControlDirectives, 'max-age')) {
- return 0
- }
- const maxAge = cacheControlDirectives['max-age']
- if (maxAge !== undefined) {
- return maxAge * 1000
- }
- if (Object.hasOwn(resHeaders, 'expires')) {
- // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
- if (typeof resHeaders.expires !== 'string') {
- return 0
- }
- const expiresDate = parseHttpDate(resHeaders.expires)
- if (!expiresDate) {
- return 0
- }
- if (now >= expiresDate.getTime()) {
- return 0
- }
- if (responseDate) {
- if (responseDate >= expiresDate) {
- return 0
- }
- const freshnessLifetime = expiresDate.getTime() - responseDate.getTime()
- if (age !== undefined && age >= freshnessLifetime) {
- return 0
- }
- return freshnessLifetime
- }
- return expiresDate.getTime() - now
- }
- if (typeof resHeaders['last-modified'] === 'string') {
- // https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-heuristic-fresh
- const lastModified = parseHttpDate(resHeaders['last-modified'])
- if (lastModified) {
- if (lastModified.getTime() >= now) {
- return undefined
- }
- const responseAge = now - lastModified.getTime()
- return responseAge * 0.1
- }
- }
- if (cacheControlDirectives.immutable) {
- // https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
- return 31536000000
- }
- return undefined
- }
- /**
- * @param {number} baseTime
- * @param {number} cachedAt
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
- * @param {number} staleAt
- */
- function determineDeleteAt (baseTime, cachedAt, cacheControlDirectives, staleAt) {
- let staleWhileRevalidate = -Infinity
- let staleIfError = -Infinity
- let immutable = -Infinity
- if (cacheControlDirectives['stale-while-revalidate']) {
- staleWhileRevalidate = staleAt + (cacheControlDirectives['stale-while-revalidate'] * 1000)
- }
- if (cacheControlDirectives['stale-if-error']) {
- staleIfError = staleAt + (cacheControlDirectives['stale-if-error'] * 1000)
- }
- if (cacheControlDirectives.immutable && staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
- immutable = cachedAt + 31536000000
- }
- // When no stale directives or immutable flag, add a revalidation buffer
- // equal to the freshness lifetime so the entry survives past staleAt long
- // enough to be revalidated instead of silently disappearing.
- //
- // Response Date headers only have second precision, so baseTime can trail the
- // actual cache insertion time by up to ~1s. Pad the buffer by that bounded
- // skew so short-lived entries do not disappear exactly when they should be
- // revalidated.
- if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity && immutable === -Infinity) {
- const freshnessLifetime = staleAt - baseTime
- const datePrecisionPadding = Math.min(Math.max(cachedAt - baseTime, 0), 1000)
- return staleAt + freshnessLifetime + datePrecisionPadding
- }
- return Math.max(staleAt, staleWhileRevalidate, staleIfError, immutable)
- }
- /**
- * Strips headers required to be removed in cached responses
- * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
- * @returns {Record<string, string | string []>}
- */
- function stripNecessaryHeaders (resHeaders, cacheControlDirectives) {
- const headersToRemove = [
- 'connection',
- 'proxy-authenticate',
- 'proxy-authentication-info',
- 'proxy-authorization',
- 'proxy-connection',
- 'te',
- 'transfer-encoding',
- 'upgrade',
- // We'll add age back when serving it
- 'age'
- ]
- if (resHeaders['connection']) {
- appendConnectionHeaderTokens(headersToRemove, resHeaders['connection'])
- }
- if (Array.isArray(cacheControlDirectives['no-cache'])) {
- headersToRemove.push(...cacheControlDirectives['no-cache'])
- }
- if (Array.isArray(cacheControlDirectives['private'])) {
- headersToRemove.push(...cacheControlDirectives['private'])
- }
- let strippedHeaders
- for (const headerName of headersToRemove) {
- if (Object.hasOwn(resHeaders, headerName)) {
- strippedHeaders ??= { ...resHeaders }
- delete strippedHeaders[headerName]
- }
- }
- return strippedHeaders ?? resHeaders
- }
- module.exports = CacheHandler
|