diff --git a/lib/handler/cache-handler.js b/lib/handler/cache-handler.js index e02ff9c9d72..b8a882381c2 100644 --- a/lib/handler/cache-handler.js +++ b/lib/handler/cache-handler.js @@ -4,7 +4,8 @@ const util = require('../core/util') const { parseCacheControlHeader, parseVaryHeader, - isEtagUsable + isEtagUsable, + makeHeaderNamesLowercase } = require('../util/cache') const { parseHttpDate } = require('../util/date.js') @@ -111,11 +112,13 @@ class CacheHandler { return downstreamOnHeaders() } - const cacheControlHeader = resHeaders['cache-control'] - const heuristicallyCacheable = resHeaders['last-modified'] && HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode) + const resHeadersLowercase = makeHeaderNamesLowercase(resHeaders) + + const cacheControlHeader = resHeadersLowercase['cache-control'] + const heuristicallyCacheable = resHeadersLowercase['last-modified'] && HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode) if ( !cacheControlHeader && - !resHeaders['expires'] && + !resHeadersLowercase.expires && !heuristicallyCacheable && !this.#cacheByDefault ) { @@ -125,23 +128,23 @@ class CacheHandler { } const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {} - if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives)) { + if (!canCacheResponse(this.#cacheType, statusCode, resHeadersLowercase, cacheControlDirectives)) { return downstreamOnHeaders() } const now = Date.now() - const resAge = resHeaders.age ? getAge(resHeaders.age) : undefined + const resAge = resHeadersLowercase.age ? getAge(resHeadersLowercase.age) : undefined if (resAge && resAge >= MAX_RESPONSE_AGE) { // Response considered stale return downstreamOnHeaders() } - const resDate = typeof resHeaders.date === 'string' - ? parseHttpDate(resHeaders.date) + const resDate = typeof resHeadersLowercase.date === 'string' + ? parseHttpDate(resHeadersLowercase.date) : undefined const staleAt = - determineStaleAt(this.#cacheType, now, resAge, resHeaders, resDate, cacheControlDirectives) ?? + determineStaleAt(this.#cacheType, now, resAge, resHeadersLowercase, resDate, cacheControlDirectives) ?? this.#cacheByDefault if (staleAt === undefined || (resAge && resAge > staleAt)) { return downstreamOnHeaders() @@ -155,8 +158,9 @@ class CacheHandler { } let varyDirectives - if (this.#cacheKey.headers && resHeaders.vary) { - varyDirectives = parseVaryHeader(resHeaders.vary, this.#cacheKey.headers) + if (this.#cacheKey.headers && resHeadersLowercase.vary) { + varyDirectives = parseVaryHeader(resHeadersLowercase.vary, this.#cacheKey.headers) + if (!varyDirectives) { // Parse error return downstreamOnHeaders() @@ -164,7 +168,7 @@ class CacheHandler { } const deleteAt = determineDeleteAt(baseTime, cacheControlDirectives, absoluteStaleAt) - const strippedHeaders = stripNecessaryHeaders(resHeaders, cacheControlDirectives) + const strippedHeaders = stripNecessaryHeaders(resHeadersLowercase, cacheControlDirectives) /** * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue} @@ -180,8 +184,8 @@ class CacheHandler { deleteAt } - if (typeof resHeaders.etag === 'string' && isEtagUsable(resHeaders.etag)) { - value.etag = resHeaders.etag + if (typeof resHeadersLowercase.etag === 'string' && isEtagUsable(resHeadersLowercase.etag)) { + value.etag = resHeadersLowercase.etag } this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value) diff --git a/lib/util/cache.js b/lib/util/cache.js index 35c53512b2a..175f9cfe49a 100644 --- a/lib/util/cache.js +++ b/lib/util/cache.js @@ -38,7 +38,7 @@ function makeCacheKey (opts) { origin: opts.origin.toString(), method: opts.method, path: opts.path, - headers + headers: makeHeaderNamesLowercase(headers) } } @@ -347,6 +347,20 @@ function assertCacheMethods (methods, name = 'CacheMethods') { } } +/** + * @param {import('../../types/header.d.ts').IncomingHttpHeaders} headers + * @returns {import('../../types/header.d.ts').IncomingHttpHeaders} + */ +function makeHeaderNamesLowercase (headers) { + const lowercased = {} + + for (const header of Object.keys(headers)) { + lowercased[header.toLowerCase()] = headers[header] + } + + return lowercased +} + module.exports = { makeCacheKey, assertCacheKey, @@ -355,5 +369,6 @@ module.exports = { parseVaryHeader, isEtagUsable, assertCacheMethods, - assertCacheStore + assertCacheStore, + makeHeaderNamesLowercase } diff --git a/test/interceptors/cache.js b/test/interceptors/cache.js index dc120c60b7b..9a77fa1600f 100644 --- a/test/interceptors/cache.js +++ b/test/interceptors/cache.js @@ -128,6 +128,51 @@ describe('Cache Interceptor', () => { } }) + test('vary directives are case-insensitive', async () => { + let requestsToOrigin = 0 + const server = createServer((_, res) => { + requestsToOrigin++ + + res.setHeader('date', 0) + res.setHeader('cache-control', 'max-age=5000') + res.setHeader('vary', 'FoO, bar, bAZ') + + res.end('asd') + }).listen(0) + + const client = new Client(`http://localhost:${server.address().port}`) + .compose(interceptors.cache()) + + after(async () => { + server.close() + await client.close() + }) + + await once(server, 'listening') + + strictEqual(requestsToOrigin, 0) + + /** + * @type {import('../../types/dispatcher').default.RequestOptions} + */ + const request = { + origin: 'localhost', + method: 'GET', + path: '/', + headers: { + Foo: '1', + BAr: 'abc', + BAZ: '789' + } + } + + await client.request(request) + equal(requestsToOrigin, 1) + + await client.request(request) + equal(requestsToOrigin, 1) + }) + test('stale responses are revalidated before deleteAt (if-modified-since)', async () => { const clock = FakeTimers.install({ shouldClearNativeTimers: true