Skip to content

Commit fa959b8

Browse files
authored
cache: ensure vary & revalidation headers are case-insensitive (#4112)
Closes #4103 Co-authored-by: alxndrsn <alxndrsn>
1 parent 33daab9 commit fa959b8

File tree

3 files changed

+104
-30
lines changed

3 files changed

+104
-30
lines changed

lib/interceptor/cache.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const util = require('../core/util')
66
const CacheHandler = require('../handler/cache-handler')
77
const MemoryCacheStore = require('../cache/memory-cache-store')
88
const CacheRevalidationHandler = require('../handler/cache-revalidation-handler')
9-
const { assertCacheStore, assertCacheMethods, makeCacheKey, parseCacheControlHeader } = require('../util/cache.js')
9+
const { assertCacheStore, assertCacheMethods, makeCacheKey, normaliseHeaders, parseCacheControlHeader } = require('../util/cache.js')
1010
const { AbortError } = require('../core/errors.js')
1111

1212
/**
@@ -233,7 +233,7 @@ function handleResult (
233233
}
234234

235235
let headers = {
236-
...opts.headers,
236+
...normaliseHeaders(opts),
237237
'if-modified-since': new Date(result.cachedAt).toUTCString()
238238
}
239239

lib/util/cache.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,21 @@ function makeCacheKey (opts) {
1212
throw new Error('opts.origin is undefined')
1313
}
1414

15-
/** @type {Record<string, string[] | string>} */
15+
const headers = normaliseHeaders(opts)
16+
17+
return {
18+
origin: opts.origin.toString(),
19+
method: opts.method,
20+
path: opts.path,
21+
headers
22+
}
23+
}
24+
25+
/**
26+
* @param {Record<string, string[] | string>}
27+
* @return {Record<string, string[] | string>}
28+
*/
29+
function normaliseHeaders (opts) {
1630
let headers
1731
if (opts.headers == null) {
1832
headers = {}
@@ -38,12 +52,7 @@ function makeCacheKey (opts) {
3852
throw new Error('opts.headers is not an object')
3953
}
4054

41-
return {
42-
origin: opts.origin.toString(),
43-
method: opts.method,
44-
path: opts.path,
45-
headers
46-
}
55+
return headers
4756
}
4857

4958
/**
@@ -350,6 +359,7 @@ function assertCacheMethods (methods, name = 'CacheMethods') {
350359

351360
module.exports = {
352361
makeCacheKey,
362+
normaliseHeaders,
353363
assertCacheKey,
354364
assertCacheValue,
355365
parseCacheControlHeader,

test/interceptors/cache.js

Lines changed: 85 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -135,22 +135,30 @@ describe('Cache Interceptor', () => {
135135

136136
let requestsToOrigin = 0
137137
let revalidationRequests = 0
138+
let serverError
138139
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
139140
res.setHeader('date', 0)
140141
res.setHeader('cache-control', 's-maxage=1, stale-while-revalidate=10')
141142

142-
if (req.headers['if-modified-since']) {
143-
revalidationRequests++
143+
try {
144+
if (req.headers['if-modified-since']) {
145+
equal(req.headers['if-modified-since'].length, 29)
146+
147+
revalidationRequests++
144148

145-
if (revalidationRequests === 2) {
146-
res.end('updated')
149+
if (revalidationRequests === 3) {
150+
res.end('updated')
151+
} else {
152+
res.statusCode = 304
153+
res.end()
154+
}
147155
} else {
148-
res.statusCode = 304
149-
res.end()
156+
requestsToOrigin++
157+
res.end('asd')
150158
}
151-
} else {
152-
requestsToOrigin++
153-
res.end('asd')
159+
} catch (err) {
160+
serverError = err
161+
res.end()
154162
}
155163
}).listen(0)
156164

@@ -188,6 +196,10 @@ describe('Cache Interceptor', () => {
188196
// Send initial request. This should reach the origin
189197
{
190198
const res = await client.request(request)
199+
if (serverError) {
200+
throw serverError
201+
}
202+
191203
equal(requestsToOrigin, 1)
192204
equal(revalidationRequests, 0)
193205
strictEqual(await res.body.text(), 'asd')
@@ -198,16 +210,42 @@ describe('Cache Interceptor', () => {
198210
// Response is now stale, the origin should get a revalidation request
199211
{
200212
const res = await client.request(request)
213+
if (serverError) {
214+
throw serverError
215+
}
216+
201217
equal(requestsToOrigin, 1)
202218
equal(revalidationRequests, 1)
203219
strictEqual(await res.body.text(), 'asd')
204220
}
205221

222+
// Response is still stale, extra header should be overwritten, and the
223+
// origin should get a revalidation request
224+
{
225+
const res = await client.request({
226+
...request,
227+
headers: {
228+
'if-modified-SINCE': 'Thu, 01 Jan 1970 00:00:00 GMT'
229+
}
230+
})
231+
if (serverError) {
232+
throw serverError
233+
}
234+
235+
equal(requestsToOrigin, 1)
236+
equal(revalidationRequests, 2)
237+
strictEqual(await res.body.text(), 'asd')
238+
}
239+
206240
// Response is still stale, but revalidation should fail now.
207241
{
208242
const res = await client.request(request)
243+
if (serverError) {
244+
throw serverError
245+
}
246+
209247
equal(requestsToOrigin, 1)
210-
equal(revalidationRequests, 2)
248+
equal(revalidationRequests, 3)
211249
strictEqual(await res.body.text(), 'updated')
212250
}
213251
})
@@ -230,7 +268,7 @@ describe('Cache Interceptor', () => {
230268

231269
equal(req.headers['if-none-match'], '"asd123"')
232270

233-
if (revalidationRequests === 2) {
271+
if (revalidationRequests === 3) {
234272
res.end('updated')
235273
} else {
236274
res.statusCode = 304
@@ -296,6 +334,24 @@ describe('Cache Interceptor', () => {
296334
strictEqual(await res.body.text(), 'asd')
297335
}
298336

337+
// Response is still stale, extra headers should be overwritten, and the
338+
// origin should get a revalidation request
339+
{
340+
const res = await client.request({
341+
...request,
342+
headers: {
343+
'if-NONE-match': '"nonsense-etag"'
344+
}
345+
})
346+
if (serverError) {
347+
throw serverError
348+
}
349+
350+
equal(requestsToOrigin, 1)
351+
equal(revalidationRequests, 2)
352+
strictEqual(await res.body.text(), 'asd')
353+
}
354+
299355
// Response is still stale, but revalidation should fail now.
300356
{
301357
const res = await client.request(request)
@@ -304,7 +360,7 @@ describe('Cache Interceptor', () => {
304360
}
305361

306362
equal(requestsToOrigin, 1)
307-
equal(revalidationRequests, 2)
363+
equal(revalidationRequests, 3)
308364
strictEqual(await res.body.text(), 'updated')
309365
}
310366
})
@@ -327,13 +383,13 @@ describe('Cache Interceptor', () => {
327383
if (ifNoneMatch) {
328384
revalidationRequests++
329385
notEqual(req.headers.a, undefined)
330-
notEqual(req.headers.b, undefined)
386+
notEqual(req.headers['b-mixed-case'], undefined)
331387

332388
res.statusCode = 304
333389
res.end()
334390
} else {
335391
requestsToOrigin++
336-
res.setHeader('vary', 'a, b')
392+
res.setHeader('vary', 'a, B-MIXED-CASe')
337393
res.setHeader('etag', '"asd"')
338394
res.end('asd')
339395
}
@@ -360,15 +416,17 @@ describe('Cache Interceptor', () => {
360416
const request = {
361417
origin: 'localhost',
362418
path: '/',
363-
method: 'GET',
364-
headers: {
365-
a: 'asd',
366-
b: 'asd'
367-
}
419+
method: 'GET'
368420
}
369421

370422
{
371-
const response = await client.request(request)
423+
const response = await client.request({
424+
...request,
425+
headers: {
426+
a: 'asd',
427+
'b-Mixed-case': 'asd'
428+
}
429+
})
372430
if (serverError) {
373431
throw serverError
374432
}
@@ -380,7 +438,13 @@ describe('Cache Interceptor', () => {
380438
clock.tick(1500)
381439

382440
{
383-
const response = await client.request(request)
441+
const response = await client.request({
442+
...request,
443+
headers: {
444+
a: 'asd',
445+
'B-mixed-CASE': 'asd'
446+
}
447+
})
384448
if (serverError) {
385449
throw serverError
386450
}

0 commit comments

Comments
 (0)