mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
fix(web): serve the public JSON data files no-cache, not immutable
The static plugin gave every file under public/ a year of `immutable`, including the JSON the page fetches at runtime: whats-new.json (rewritten every release), the new security-notices.json (edited by hand) and manifest.json. None of them is content-hashed or `?v=`-busted, so a browser could keep last release's copy, and a security notice added in a later release might never reach it. A `.json` file now revalidates (`no-cache`); every other static asset keeps its immutable year, and a route that set its own Cache-Control still wins. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
0ab71d61d1
commit
ada2309721
2 files changed
+23
-2
No files matched your search
+10
-1
@@ -964,7 +964,7 @@ export class WebServer extends EventEmitter {
|
||||
// `ServerResponse` to a `FastifyReply`, so it is `reply.header()` here and
|
||||
// NOT `res.setHeader()`. A v9-style body throws TypeError on every static
|
||||
// request, which is every page load. See the v10.0.0 release notes.
|
||||
setHeaders: (reply) => {
|
||||
setHeaders: (reply, filePath) => {
|
||||
// ⚠️ That same change ALSO flipped precedence, and silently. Under v9 this
|
||||
// callback wrote to the raw response and Fastify's staged reply headers then
|
||||
// overwrote it, so a route that set its own Cache-Control before .sendFile()
|
||||
@@ -973,6 +973,15 @@ export class WebServer extends EventEmitter {
|
||||
// no-store` its route asks for — a service worker that can never update.
|
||||
// So: a route that already decided keeps its answer.
|
||||
if (reply.getHeader('Cache-Control') !== undefined) return;
|
||||
// A JSON file here is DATA the page fetches at runtime (whats-new.json is rewritten every
|
||||
// release, security-notices.json by hand, manifest.json), never content-hashed and never
|
||||
// `?v=`-busted, so it must revalidate. Under `immutable` a browser may keep last release's
|
||||
// copy for a year, and the page's `fetch(..., { cache: 'no-cache' })` is the only thing
|
||||
// asking it not to; the response header is what every browser honours.
|
||||
if (filePath.endsWith('.json')) {
|
||||
reply.header('Cache-Control', 'no-cache');
|
||||
return;
|
||||
}
|
||||
reply.header('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
*
|
||||
* That contract is load-bearing: assets are served `immutable` for a year, and
|
||||
* `index.html` must revalidate every time or a deploy leaves browsers on stale
|
||||
* markup (see `cacheBustAssets` in server.ts). No HTML is left behind the static
|
||||
* markup (see `cacheBustAssets` in server.ts). The JSON data files the page fetches at
|
||||
* runtime (whats-new.json, security-notices.json) revalidate too. No HTML is left behind the static
|
||||
* plugin (every page has its own route), so the HTML half is asserted on the route.
|
||||
*
|
||||
* These tests drive a REAL WebServer on purpose. Asserting against an inline
|
||||
@@ -73,6 +74,17 @@ describe('static asset Cache-Control headers', () => {
|
||||
expect(res.headers.get('cache-control')).toBe('no-cache, no-store');
|
||||
});
|
||||
|
||||
it('makes the runtime JSON data files revalidate instead of pinning them for a year', async () => {
|
||||
// whats-new.json is rewritten every release and security-notices.json by hand; neither is
|
||||
// content-hashed or `?v=`-busted, so an `immutable` copy could outlive the release that
|
||||
// replaced it, and a security notice added later would never reach that browser.
|
||||
for (const file of ['security-notices.json', 'whats-new.json', 'manifest.json']) {
|
||||
const res = await get(`${baseUrl}/${file}`);
|
||||
expect(res.status, file).toBe(200);
|
||||
expect(res.headers.get('cache-control'), file).toBe('no-cache');
|
||||
}
|
||||
});
|
||||
|
||||
it('serves the rendered index with no-cache too, via its own route', async () => {
|
||||
// Different code path from the above, same contract: index.html is generated per
|
||||
// request (cacheBustAssets stamps ?v= onto every asset ref), so caching it would
|
||||
|
||||
Reference in new issue
Block a user