import { type Context, Hono } from 'hono' import { createMiddleware } from 'hono/factory' import * as EdgeCache from './EdgeCache.js' import type * as Store from './Store.js' describe('middleware', () => { test('serves GETs from cache, skipping downstream middleware', async () => { const { app, store, downstreamCalls } = createApp() const first = await app.request('/cached') const second = await app.request('/cached') // The second request is served from the edge cache, so the downstream // (auth/rate-limit) middleware runs only once. expect(downstreamCalls()).toMatchInlineSnapshot(`1`) expect(store.size).toMatchInlineSnapshot(`1`) expect(await first.json()).toMatchInlineSnapshot(` { "ok": true, } `) expect(await second.json()).toMatchInlineSnapshot(` { "ok": true, } `) // The miss returns the route's own (private, metered) response; the stored // copy served on the hit is public and stripped of per-request headers. expect(first.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`"9"`) expect(second.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`null`) expect(second.headers.get('server-timing')).toMatchInlineSnapshot(`null`) expect(second.headers.get('cache-control')).toMatchInlineSnapshot( `"public, max-age=30, s-maxage=30, stale-while-revalidate=300"`, ) expect(second.headers.get('vary')).toMatchInlineSnapshot(`"Accept-Encoding"`) }) test('bypasses the cache for credential headers', async () => { const { app, store, downstreamCalls } = createApp() await app.request('/cached', { headers: { authorization: 'Bearer one' } }) await app.request('/cached', { headers: { authorization: 'Basic malformed' } }) await app.request('/cached', { headers: { authorization: 'Payment paid' } }) await app.request('/cached', { headers: { 'tempo-api-key': 'two' } }) await app.request('/cached', { headers: { 'x-api-key': 'three' } }) expect(downstreamCalls()).toMatchInlineSnapshot(`5`) expect(store.gets).toMatchInlineSnapshot(`0`) expect(store.size).toMatchInlineSnapshot(`0`) }) test('bypasses the cache for query API key credentials', async () => { const { app, store, downstreamCalls } = createApp() await app.request('/cached?key=opaque-secret') await app.request('/cached?key=opaque-secret') expect(downstreamCalls()).toMatchInlineSnapshot(`2`) expect(store.gets).toMatchInlineSnapshot(`0`) expect(store.size).toMatchInlineSnapshot(`0`) }) test('refreshes the entry when the request sends `Cache-Control: no-cache`', async () => { const { app, store, downstreamCalls } = createApp() await app.request('/cached') // `no-cache` skips the stored hit, re-runs downstream, and overwrites the // entry with the fresh response. const refreshed = await app.request('/cached', { headers: { 'cache-control': 'no-cache' }, }) const after = await app.request('/cached') expect(downstreamCalls()).toMatchInlineSnapshot(`2`) expect(store.size).toMatchInlineSnapshot(`1`) // The refresh returns the route's own (private, metered) response, and the // following request is served from the rewritten entry. expect(refreshed.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`"9"`) expect(after.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`null`) }) test('refreshes the entry when the request sends `Pragma: no-cache`', async () => { const { app, store, downstreamCalls } = createApp() await app.request('/cached') // `Pragma` is the HTTP/1.0 fallback for `Cache-Control: no-cache`. await app.request('/cached', { headers: { pragma: 'no-cache' } }) expect(downstreamCalls()).toMatchInlineSnapshot(`2`) expect(store.size).toMatchInlineSnapshot(`1`) }) test('strips CDN telemetry headers from stored copies', async () => { const { app } = createApp() // The miss response carries `Age`/`CF-Cache-Status` (e.g. stamped by // Cloudflare on an inner Cache API hit); the stored copy must not freeze // them, or every later hit would report the same stale telemetry. await app.request('/cached') const hit = await app.request('/cached') expect(hit.headers.get('age')).toMatchInlineSnapshot(`null`) expect(hit.headers.get('cf-cache-status')).toMatchInlineSnapshot(`null`) }) test('bypasses the cache entirely when the request sends `Cache-Control: no-store`', async () => { const { app, store, downstreamCalls } = createApp() await app.request('/cached') // `no-store` neither serves nor writes the cache: the entry from the first // request stays, but this request always runs downstream. const bypassed = await app.request('/cached', { headers: { 'cache-control': 'no-store' }, }) expect(downstreamCalls()).toMatchInlineSnapshot(`2`) expect(store.size).toMatchInlineSnapshot(`1`) expect(bypassed.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`"9"`) }) test('does not cache non-2xx responses', async () => { const { app, store, downstreamCalls } = createApp() await app.request('/error') await app.request('/error') expect(downstreamCalls()).toMatchInlineSnapshot(`2`) expect(store.size).toMatchInlineSnapshot(`0`) }) test('strips payment headers from private responses before sharing them', async () => { const { app, store, downstreamCalls } = createApp() const miss = await app.request('/private') const hit = await app.request('/private') expect(downstreamCalls()).toMatchInlineSnapshot(`1`) expect(store.size).toMatchInlineSnapshot(`1`) expect(miss.headers.get('payment-receipt')).toMatchInlineSnapshot(`"payer-specific"`) expect(miss.headers.get('payment-session-snapshot')).toMatchInlineSnapshot(`"session-specific"`) expect(hit.headers.get('payment-receipt')).toMatchInlineSnapshot(`null`) expect(hit.headers.get('payment-session-snapshot')).toMatchInlineSnapshot(`null`) expect(hit.headers.get('cache-control')).toMatchInlineSnapshot( `"public, max-age=30, s-maxage=30"`, ) }) test('does not cache responses marked `no-store`', async () => { const { app, store, downstreamCalls } = createApp() await app.request('/no-store') await app.request('/no-store') expect(downstreamCalls()).toMatchInlineSnapshot(`2`) expect(store.size).toMatchInlineSnapshot(`0`) }) test('does not cache routes that did not opt in', async () => { const { app, store, downstreamCalls } = createApp() await app.request('/uncached') await app.request('/uncached') expect(downstreamCalls()).toMatchInlineSnapshot(`2`) expect(store.size).toMatchInlineSnapshot(`0`) }) test('skips the store lookup for routes that did not opt in', async () => { const { app, store } = createApp() await app.request('/uncached') await app.request('/missing-route') expect(store.gets).toMatchInlineSnapshot(`0`) // Marked routes still pay (and use) the lookup. await app.request('/cached') expect(store.gets).toMatchInlineSnapshot(`1`) }) test('bypasses the cache when its route key cannot be generated', async () => { const { app, store, downstreamCalls } = createApp({ key: () => { throw new Error('invalid query') }, }) const first = await app.request('/cached') const second = await app.request('/cached') expect(first.status).toMatchInlineSnapshot(`200`) expect(second.status).toMatchInlineSnapshot(`200`) expect(downstreamCalls()).toMatchInlineSnapshot(`2`) expect(store.gets).toMatchInlineSnapshot(`0`) expect(store.size).toMatchInlineSnapshot(`0`) }) test('answers conditional requests from a cached ETag with 304', async () => { const { app } = createApp() const first = await app.request('/cached') const etag = first.headers.get('etag') ?? '' const conditional = await app.request('/cached', { headers: { 'if-none-match': etag } }) expect(etag).toMatchInlineSnapshot(`""abc""`) expect(conditional.status).toMatchInlineSnapshot(`304`) expect(await conditional.text()).toMatchInlineSnapshot(`""`) expect(conditional.headers.get('etag')).toMatchInlineSnapshot(`""abc""`) }) test('round-trips binary bodies through the text store without corruption', async () => { const { app, store, downstreamCalls } = createApp() const miss = await app.request('/binary') const hit = await app.request('/binary') // The second request is served from the cache (downstream ran once). expect(downstreamCalls()).toMatchInlineSnapshot(`1`) expect(store.size).toMatchInlineSnapshot(`1`) expect(hit.headers.get('content-type')).toMatchInlineSnapshot(`"image/png"`) const expected = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0x00] expect([...new Uint8Array(await miss.arrayBuffer())]).toEqual(expected) expect([...new Uint8Array(await hit.arrayBuffer())]).toEqual(expected) }) test('is a no-op when no edge cache is available', async () => { const { app, downstreamCalls } = createApp({ store: undefined }) await app.request('/cached') await app.request('/cached') expect(downstreamCalls()).toMatchInlineSnapshot(`2`) }) test('rechecks route eligibility before every cache lookup', async () => { let eligible = true const { app, downstreamCalls, store } = createApp({ eligibility: [() => eligible] }) await app.request('/cached') eligible = false await app.request('/cached') expect(downstreamCalls()).toBe(2) expect(store.gets).toBe(1) expect(store.size).toBe(1) }) test('requires every route eligibility predicate to permit caching', async () => { const { app, downstreamCalls, store } = createApp({ eligibility: [() => true, () => false], }) await app.request('/cached') await app.request('/cached') expect(downstreamCalls()).toBe(2) expect(store.gets).toBe(0) expect(store.size).toBe(0) }) }) type Environment = { Variables: EdgeCache.Variables } // In-memory `Store.Store` so the edge cache and origin cache share one shape. function memoryStore() { const map = new Map() let gets = 0 return { async delete(key: string) { map.delete(key) }, async get(key: string) { gets++ return map.get(key) ?? null }, get gets() { return gets }, async list() { return { keys: [...map.keys()].map((name) => ({ name })) } }, async put(key: string, value: string) { map.set(key, value) }, get size() { return map.size }, type: 'state', } satisfies Store.Store & { readonly gets: number; readonly size: number } } function createApp( options: { eligibility?: readonly ((c: Context) => boolean)[] | undefined key?: ((c: Context) => Promise | string) | undefined store?: Store.Store | undefined } = {}, ) { const store = memoryStore() const edgeStore = 'store' in options ? options.store : store let downstream = 0 const app = new Hono() app.use('*', EdgeCache.middleware({ store: edgeStore })) app.use('*', async (_c, next) => { downstream++ await next() }) // Routes publishing a policy mount a marked middleware, as `Cache.response` // routes do; unmarked routes must skip the lookup entirely. const cacheable_marked = EdgeCache.markCacheable( createMiddleware(async (_c, next) => next()), { key: options.key as (c: Context) => Promise | string }, ) for (const predicate of options.eligibility ?? []) EdgeCache.setEligibility(cacheable_marked, predicate) const cacheable = cacheable_marked app.get('/cached', cacheable, (c) => { c.set('edgeCache', { maxAge: 30, staleWhileRevalidate: 300 }) c.header('Age', '123') c.header('CF-Cache-Status', 'HIT') c.header('ETag', '"abc"') c.header('Server-Timing', 'app;dur=1') c.header('RateLimit-Remaining', '9') return c.json({ ok: true }) }) app.get('/error', cacheable, (c) => { c.set('edgeCache', { maxAge: 30, staleWhileRevalidate: 300 }) return c.json({ ok: false }, 500) }) app.get('/private', cacheable, (c) => { c.set('edgeCache', { maxAge: 30 }) c.header('Cache-Control', 'private, max-age=30') c.header('Payment-Receipt', 'payer-specific') c.header('Payment-Session-Snapshot', 'session-specific') return c.json({ ok: true }) }) app.get('/no-store', cacheable, (c) => { c.set('edgeCache', { maxAge: 30 }) c.header('Cache-Control', 'no-store') c.header('Payment-Receipt', 'payer-specific') return c.json({ ok: true }) }) app.get('/binary', cacheable, (c) => { c.set('edgeCache', { maxAge: 30, staleWhileRevalidate: 300 }) // Bytes that are not valid UTF-8 (PNG signature) — corrupted if stored via // `.text()` instead of base64. return c.body( new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0x00]), 200, { 'Content-Type': 'image/png', }, ) }) app.get('/uncached', (c) => c.json({ ok: true })) return { app, store, downstreamCalls: () => downstream } }