Skip to content

Commit 12c5117

Browse files
authored
Merge commit from fork
* fix(cache): prevent caching of private responses and set-cookie headers * test(cache): fix no-cache test case * perf(cache): implement regex check for cache control header
1 parent 7343487 commit 12c5117

File tree

2 files changed

+100
-4
lines changed

2 files changed

+100
-4
lines changed

src/middleware/cache/index.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,3 +417,86 @@ describe('Cache Middleware', () => {
417417
expect(res.headers.get('cache-control')).toBe(null)
418418
})
419419
})
420+
421+
describe('Cache Skipping Logic', () => {
422+
let putSpy: ReturnType<typeof vi.fn>
423+
424+
beforeEach(() => {
425+
putSpy = vi.fn()
426+
const mockCache = {
427+
match: vi.fn().mockResolvedValue(undefined), // Always miss
428+
put: putSpy, // We spy on this
429+
keys: vi.fn().mockResolvedValue([]),
430+
}
431+
432+
vi.stubGlobal('caches', {
433+
open: vi.fn().mockResolvedValue(mockCache),
434+
})
435+
})
436+
437+
afterEach(() => {
438+
vi.restoreAllMocks()
439+
})
440+
441+
it('Should NOT cache response if Cache-Control contains "private"', async () => {
442+
const app = new Hono()
443+
app.use('*', cache({ cacheName: 'skip-test', wait: true }))
444+
app.get('/', (c) => {
445+
c.header('Cache-Control', 'private, max-age=3600')
446+
return c.text('response')
447+
})
448+
449+
const res = await app.request('/')
450+
expect(res.status).toBe(200)
451+
// IMPORTANT: put() should NOT be called
452+
expect(putSpy).not.toHaveBeenCalled()
453+
})
454+
455+
it('Should NOT cache response if Cache-Control contains "no-store"', async () => {
456+
const app = new Hono()
457+
app.use('*', cache({ cacheName: 'skip-test', wait: true }))
458+
app.get('/', (c) => {
459+
c.header('Cache-Control', 'no-store')
460+
return c.text('response')
461+
})
462+
463+
await app.request('/')
464+
expect(putSpy).not.toHaveBeenCalled()
465+
})
466+
467+
it('Should NOT cache response if Cache-Control contains no-cache="Set-Cookie"', async () => {
468+
const app = new Hono()
469+
app.use('*', cache({ cacheName: 'skip-test', wait: true }))
470+
app.get('/', (c) => {
471+
c.header('Cache-Control', 'no-cache="Set-Cookie"')
472+
return c.text('response')
473+
})
474+
475+
await app.request('/')
476+
expect(putSpy).not.toHaveBeenCalled()
477+
})
478+
479+
it('Should NOT cache response if Set-Cookie header is present', async () => {
480+
const app = new Hono()
481+
app.use('*', cache({ cacheName: 'skip-test', wait: true }))
482+
app.get('/', (c) => {
483+
c.header('Set-Cookie', 'session=secret')
484+
return c.text('response')
485+
})
486+
487+
await app.request('/')
488+
expect(putSpy).not.toHaveBeenCalled()
489+
})
490+
491+
it('Should cache normal responses (Control Test)', async () => {
492+
const app = new Hono()
493+
app.use('*', cache({ cacheName: 'skip-test', wait: true }))
494+
app.get('/', (c) => {
495+
return c.text('response')
496+
})
497+
498+
await app.request('/')
499+
// IMPORTANT: put() SHOULD be called for normal responses
500+
expect(putSpy).toHaveBeenCalled()
501+
})
502+
})

src/middleware/cache/index.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,23 @@ const defaultCacheableStatusCodes: ReadonlyArray<StatusCode> = [200]
1414

1515
const shouldSkipCache = (res: Response) => {
1616
const vary = res.headers.get('Vary')
17-
// Don't cache for Vary: *
18-
// https://www.rfc-editor.org/rfc/rfc9111#section-4.1
19-
// Also note that some runtimes throw a TypeError for it.
20-
return vary && vary.includes('*')
17+
if (vary && vary.includes('*')) {
18+
return true
19+
}
20+
21+
const cacheControl = res.headers.get('Cache-Control')
22+
if (
23+
cacheControl &&
24+
/(?:^|,\s*)(?:private|no-(?:store|cache))(?:\s*(?:=|,|$))/i.test(cacheControl)
25+
) {
26+
return true
27+
}
28+
29+
if (res.headers.has('Set-Cookie')) {
30+
return true
31+
}
32+
33+
return false
2134
}
2235

2336
/**

0 commit comments

Comments
 (0)