Skip to content

Commit e65eb14

Browse files
gurgundayflakey5
andcommitted
fix: handle missing vary header values
Co-authored-by: flakey5 <73616808+flakey5@users.noreply.github.com>
1 parent 008187b commit e65eb14

File tree

6 files changed

+147
-11
lines changed

6 files changed

+147
-11
lines changed

lib/cache/memory-cache-store.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class MemoryCacheStore {
7979
const entry = this.#entries.get(topLevelKey)?.find((entry) => (
8080
entry.deleteAt > now &&
8181
entry.method === key.method &&
82-
(entry.vary == null || Object.keys(entry.vary).every(headerName => entry.vary[headerName] === key.headers?.[headerName]))
82+
(entry.vary == null || Object.keys(entry.vary).every(headerName => entry.vary[headerName] === key.headers[headerName]))
8383
))
8484

8585
return entry == null

lib/util/cache.js

+9-8
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,14 @@ function makeCacheKey (opts) {
2626
if (typeof key !== 'string' || typeof val !== 'string') {
2727
throw new Error('opts.headers is not a valid header map')
2828
}
29-
headers[key] = val
29+
headers[key.toLowerCase()] = val
3030
}
3131
} else if (typeof opts.headers === 'object') {
32-
headers = opts.headers
32+
headers = {}
33+
34+
for (const key of Object.keys(opts.headers)) {
35+
headers[key.toLowerCase()] = opts.headers[key]
36+
}
3337
} else {
3438
throw new Error('opts.headers is not an object')
3539
}
@@ -260,19 +264,16 @@ function parseVaryHeader (varyHeader, headers) {
260264
return headers
261265
}
262266

263-
const output = /** @type {Record<string, string | string[]>} */ ({})
267+
const output = /** @type {Record<string, string | string[] | undefined>} */ ({})
264268

265269
const varyingHeaders = typeof varyHeader === 'string'
266270
? varyHeader.split(',')
267271
: varyHeader
272+
268273
for (const header of varyingHeaders) {
269274
const trimmedHeader = header.trim().toLowerCase()
270275

271-
if (headers[trimmedHeader]) {
272-
output[trimmedHeader] = headers[trimmedHeader]
273-
} else {
274-
return undefined
275-
}
276+
output[trimmedHeader] = headers[trimmedHeader]
276277
}
277278

278279
return output

test/issue-3959.js

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const { describe, test, after } = require('node:test')
2+
const assert = require('node:assert')
3+
const { createServer } = require('node:http')
4+
const MemoryCacheStore = require('../lib/cache/memory-cache-store.js')
5+
const { request, Agent, setGlobalDispatcher } = require('..')
6+
const { interceptors } = require('..')
7+
8+
describe('Cache with Vary headers', () => {
9+
test('should cache response when Vary header exists but request header is missing', async () => {
10+
let requestCount = 0
11+
const server = createServer((req, res) => {
12+
requestCount++
13+
res.setHeader('Vary', 'Accept-Encoding')
14+
res.setHeader('Cache-Control', 'max-age=60')
15+
res.end(`Request count: ${requestCount}`)
16+
})
17+
18+
await new Promise(resolve => server.listen(0, resolve))
19+
const port = server.address().port
20+
const url = `http://localhost:${port}`
21+
22+
const store = new MemoryCacheStore()
23+
const agent = new Agent()
24+
setGlobalDispatcher(
25+
agent.compose(
26+
interceptors.cache({
27+
store,
28+
cacheByDefault: 1000,
29+
methods: ['GET']
30+
})
31+
)
32+
)
33+
34+
const res1 = await request(url)
35+
const body1 = await res1.body.text()
36+
assert.strictEqual(body1, 'Request count: 1')
37+
assert.strictEqual(requestCount, 1)
38+
39+
const res2 = await request(url)
40+
const body2 = await res2.body.text()
41+
assert.strictEqual(body2, 'Request count: 1')
42+
assert.strictEqual(requestCount, 1)
43+
44+
const res3 = await request(url, {
45+
headers: {
46+
'Accept-Encoding': 'gzip'
47+
}
48+
})
49+
const body3 = await res3.body.text()
50+
assert.strictEqual(body3, 'Request count: 2')
51+
assert.strictEqual(requestCount, 2)
52+
53+
after(() => server.close())
54+
})
55+
})

test/types/cache-interceptor.test-d.ts

+39
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,45 @@ expectNotAssignable<CacheInterceptor.CacheValue>({
7878
deleteAt: ''
7979
})
8080

81+
expectAssignable<CacheInterceptor.CacheValue>({
82+
statusCode: 200,
83+
statusMessage: 'OK',
84+
headers: {},
85+
vary: {
86+
'accept-encoding': undefined,
87+
authorization: 'example-value'
88+
},
89+
cachedAt: 0,
90+
staleAt: 0,
91+
deleteAt: 0
92+
})
93+
94+
expectAssignable<CacheInterceptor.CacheValue>({
95+
statusCode: 200,
96+
statusMessage: 'OK',
97+
headers: {},
98+
vary: {
99+
'accept-encoding': undefined,
100+
authorization: undefined
101+
},
102+
cachedAt: 0,
103+
staleAt: 0,
104+
deleteAt: 0
105+
})
106+
107+
expectNotAssignable<CacheInterceptor.CacheValue>({
108+
statusCode: 200,
109+
statusMessage: 'OK',
110+
headers: {},
111+
vary: {
112+
'accept-encoding': false,
113+
authorization: 'example-value'
114+
},
115+
cachedAt: 0,
116+
staleAt: 0,
117+
deleteAt: 0
118+
})
119+
81120
expectAssignable<CacheInterceptor.MemoryCacheStoreOpts>({})
82121
expectAssignable<CacheInterceptor.MemoryCacheStoreOpts>({
83122
maxSize: 0

test/utils/cache.js

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use strict'
2+
3+
const { describe, test } = require('node:test')
4+
const assert = require('node:assert')
5+
const { parseVaryHeader } = require('../../lib/util/cache.js')
6+
7+
describe('parseVaryHeader', () => {
8+
test('handles missing headers with undefined', () => {
9+
const result = parseVaryHeader('Accept-Encoding, Authorization', {})
10+
assert.deepStrictEqual(result, {
11+
'accept-encoding': undefined,
12+
authorization: undefined
13+
})
14+
})
15+
16+
test('handles mix of present and missing headers', () => {
17+
const result = parseVaryHeader('Accept-Encoding, Authorization', {
18+
authorization: 'example-value'
19+
})
20+
assert.deepStrictEqual(result, {
21+
'accept-encoding': undefined,
22+
authorization: 'example-value'
23+
})
24+
})
25+
26+
test('handles array input', () => {
27+
const result = parseVaryHeader(['Accept-Encoding', 'Authorization'], {
28+
'accept-encoding': 'gzip'
29+
})
30+
assert.deepStrictEqual(result, {
31+
'accept-encoding': 'gzip',
32+
authorization: undefined
33+
})
34+
})
35+
36+
test('preserves existing * behavior', () => {
37+
const headers = { accept: 'text/html' }
38+
const result = parseVaryHeader('*', headers)
39+
assert.deepStrictEqual(result, headers)
40+
})
41+
})

types/cache-interceptor.d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ declare namespace CacheHandler {
7070
statusCode: number
7171
statusMessage: string
7272
headers: Record<string, string | string[]>
73-
vary?: Record<string, string | string[]>
73+
vary?: Record<string, string | string[] | undefined>
7474
etag?: string
7575
cacheControlDirectives?: CacheControlDirectives
7676
cachedAt: number
@@ -88,7 +88,7 @@ declare namespace CacheHandler {
8888
statusCode: number
8989
statusMessage: string
9090
headers: Record<string, string | string[]>
91-
vary?: Record<string, string | string[]>
91+
vary?: Record<string, string | string[] | null>
9292
etag?: string
9393
body?: Readable | Iterable<Buffer> | AsyncIterable<Buffer> | Buffer | Iterable<string> | AsyncIterable<string> | string
9494
cacheControlDirectives: CacheControlDirectives,

0 commit comments

Comments
 (0)