diff --git a/apps/web/src/lib/server/content-cache.test.ts b/apps/web/src/lib/server/content-cache.test.ts index 181ef2f..88e3b84 100644 --- a/apps/web/src/lib/server/content-cache.test.ts +++ b/apps/web/src/lib/server/content-cache.test.ts @@ -121,4 +121,31 @@ describe('content cache policy', () => { Effect.runPromise(matchEdgeCache(platform, request)) ).resolves.toBeInstanceOf(Response); }); + + it('hands back a mutable copy so hooks can add security headers', async () => { + const immutable = new Response('cached-bytes', { + status: 200, + headers: { 'Cache-Control': PUBLIC_IMMUTABLE_CACHE_CONTROL, ETag: '"abc"' } + }); + // Mirror the Workers Cache API, whose responses reject header writes. + immutable.headers.set = () => { + throw new TypeError("Can't modify immutable headers."); + }; + const platform = { + caches: { default: { match: vi.fn(async () => immutable), put: vi.fn() } } + }; + const request = new Request('https://files.example/f/id?v=1'); + + const cached = await Effect.runPromise(matchEdgeCache(platform, request)); + expect(cached).toBeInstanceOf(Response); + expect(cached).not.toBe(immutable); + expect(cached?.headers.get('ETag')).toBe('"abc"'); + expect(cached?.headers.get('Cache-Control')).toBe( + PUBLIC_IMMUTABLE_CACHE_CONTROL + ); + expect(() => + cached?.headers.set('Strict-Transport-Security', 'max-age=1') + ).not.toThrow(); + await expect(cached?.text()).resolves.toBe('cached-bytes'); + }); }); diff --git a/apps/web/src/lib/server/content-cache.ts b/apps/web/src/lib/server/content-cache.ts index bc6a6a4..3924d08 100644 --- a/apps/web/src/lib/server/content-cache.ts +++ b/apps/web/src/lib/server/content-cache.ts @@ -90,7 +90,11 @@ const workerCache = (platform: EdgePlatform) => { export const matchEdgeCache = (platform: EdgePlatform, cacheRequest: Request) => Effect.promise(async () => { try { - return await workerCache(platform)?.match(cacheRequest); + const cached = await workerCache(platform)?.match(cacheRequest); + // The Workers Cache API returns responses with immutable headers, but + // hooks.server.ts applies security headers to every response. Hand back + // a reconstructed, mutable copy so those writes don't throw. + return cached && new Response(cached.body, cached); } catch { return undefined; }