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.
This commit is contained in:
2026-09-18 16:31:08 +02:00
parent f19e596ee4
commit b9364151b0
2 changed files with 44 additions and 4 deletions
+11 -4
View File
@@ -128,11 +128,18 @@ export async function buildApp(): Promise<FastifyInstance> {
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');
}
},
});
+33
View File
@@ -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/);
});
});