From b9364151b0f1acdf15fee086334ffe3c8c8ba148 Mon Sep 17 00:00:00 2001 From: Harlekin Date: Fri, 18 Sep 2026 16:31:08 +0200 Subject: [PATCH] index.html wurde ein Jahr lang zwischengespeichert Die maxAge-Option von fastify-static setzt den Cache-Header fuer jede Datei, auch fuer die index.html. Der setHeaders-Callback, der das fuer HTML wieder aufheben sollte, kam dagegen nicht an: ausgeliefert wurde durchgehend public, max-age=31536000. Damit bekam ein Browser nach einem Update weiter die alte Oberflaeche - ein Jahr lang, ohne Fehlermeldung, obwohl der Server laengst neue Dateien hatte. Genau so ist der Abgleich-Knopf nach dem Deploy unsichtbar geblieben. Jetzt setzt die App beide Faelle selbst: HTML immer neu holen, Assets unbegrenzt behalten. Deren Dateinamen enthalten einen Inhalts-Hash, sie aendern sich bei neuem Inhalt ohnehin. Drei Tests pruefen die Header, weil ein Fehler an dieser Stelle nirgends auffaellt - er sieht aus, als waere das Update nicht angekommen. --- server/src/app.ts | 15 +++++++++++---- server/test/http.test.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/server/src/app.ts b/server/src/app.ts index a571ccc..a532a0f 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -128,11 +128,18 @@ export async function buildApp(): Promise { await app.register(fastifyStatic, { root: webRoot, prefix: '/', - // Gehashte Assets duerfen lange liegen bleiben, index.html nie. - maxAge: '1y', + // maxAge wuerde den Header fuer *alle* Dateien setzen und dabei auch die + // index.html erwischen - die muss aber bei jedem Aufruf neu geholt werden, + // sonst zeigt der Browser nach einem Update monatelang die alte Oberflaeche. + // Deshalb hier abschalten und beides unten selbst setzen. + cacheControl: false, setHeaders: (res, path) => { - if (path.endsWith('index.html')) { - res.setHeader('Cache-Control', 'no-cache'); + if (path.endsWith('.html')) { + res.setHeader('Cache-Control', 'no-cache, must-revalidate'); + } else { + // Assets tragen einen Inhalts-Hash im Namen: aendert sich der Inhalt, + // aendert sich der Dateiname. Die duerfen beliebig lange liegen. + res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); } }, }); diff --git a/server/test/http.test.ts b/server/test/http.test.ts index f0da7e8..ed3af19 100644 --- a/server/test/http.test.ts +++ b/server/test/http.test.ts @@ -286,3 +286,36 @@ describe('Guthaben ueber die API', () => { assert.equal(response.json().error, 'NOT_FOUND'); }); }); + +describe('Auslieferung der Oberflaeche', () => { + // Diese Header sind der Grund, warum ein Update beim Nutzer ankommt. Wird die + // index.html cachebar ausgeliefert, sieht er nach einem Deploy weiter die + // alte Oberflaeche - ohne dass irgendwo ein Fehler auftaucht. + it('laesst die index.html nicht zwischenspeichern', async () => { + const response = await app.inject({ method: 'GET', url: '/' }); + + assert.equal(response.statusCode, 200); + assert.match( + response.headers['cache-control'] as string, + /no-cache/, + 'die index.html muss bei jedem Aufruf neu geholt werden', + ); + }); + + it('liefert auch Deep-Links ohne Cache aus', async () => { + const response = await app.inject({ method: 'GET', url: '/players/1' }); + + assert.equal(response.statusCode, 200); + assert.match(response.headers['cache-control'] as string, /no-cache/); + }); + + it('laesst gehashte Assets lange liegen', async () => { + const page = await app.inject({ method: 'GET', url: '/' }); + const asset = /\/assets\/[^"]+\.js/.exec(page.body)?.[0]; + assert.ok(asset, 'die Seite verweist auf ein Asset'); + + const response = await app.inject({ method: 'GET', url: asset }); + assert.equal(response.statusCode, 200); + assert.match(response.headers['cache-control'] as string, /max-age=31536000/); + }); +});