Favicon-caching en 404 op ontbrekende statische bestanden
This commit is contained in:
+20
-7
@@ -5,7 +5,7 @@ import rateLimit from "express-rate-limit";
|
||||
import cookieParser from "cookie-parser";
|
||||
import pinoHttp from "pino-http";
|
||||
import { existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { extname, join, relative, sep } from "path";
|
||||
import { config } from "./config/index.js";
|
||||
import { logger } from "./logger.js";
|
||||
import { initDb } from "./db.js";
|
||||
@@ -73,21 +73,34 @@ const indexHtml = join(config.STATIC_DIR, "index.html");
|
||||
export const servesFrontend = existsSync(indexHtml);
|
||||
|
||||
if (servesFrontend) {
|
||||
// Assets hebben een hash in de naam en mogen lang gecached worden;
|
||||
// index.html juist nooit, anders blijft een oude build hangen.
|
||||
// Alleen de bundels in /assets krijgen bij elke build een nieuwe, gehashte
|
||||
// naam — die mogen een jaar blijven staan. Bestanden met een vaste naam
|
||||
// (favicon.svg, icons/*) moeten revalideren: met "immutable" blijft een
|
||||
// eenmaal verkeerd gecacht antwoord een jaar hangen, ook na een reload.
|
||||
app.use(express.static(config.STATIC_DIR, {
|
||||
index: false,
|
||||
setHeaders(res, path) {
|
||||
res.setHeader("Cache-Control", path.endsWith(".html")
|
||||
? "no-store, no-cache, must-revalidate"
|
||||
: "public, max-age=31536000, immutable");
|
||||
setHeaders(res, bestandspad) {
|
||||
const relatief = relative(config.STATIC_DIR, bestandspad).split(sep).join("/");
|
||||
if (bestandspad.endsWith(".html")) {
|
||||
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
||||
} else if (relatief.startsWith("assets/")) {
|
||||
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
||||
} else {
|
||||
res.setHeader("Cache-Control", "public, max-age=0, must-revalidate");
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
// SPA-fallback: alle overige GET-routes naar index.html. Pathloze middleware
|
||||
// in plaats van app.get("*") — dat patroon is in Express 5 ongeldig.
|
||||
//
|
||||
// Paden met een extensie vragen om een bestand (/favicon.ico, /robots.txt)
|
||||
// en krijgen een echte 404. Anders levert zo'n verzoek index.html op met
|
||||
// status 200 en content-type text/html: de browser krijgt HTML waar hij een
|
||||
// icoon verwacht en toont dan niets, zonder zichtbare fout.
|
||||
app.use((req, res, next) => {
|
||||
if (req.method !== "GET" && req.method !== "HEAD") return next();
|
||||
if (extname(req.path)) return next();
|
||||
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
||||
res.sendFile(indexHtml);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import { join } from "node:path";
|
||||
|
||||
const INDEX_MARKER = "<div id=\"root\"></div>";
|
||||
const ASSET_BODY = "console.log('vibefinance');";
|
||||
const FAVICON_BODY = "<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>";
|
||||
|
||||
let server;
|
||||
let basis;
|
||||
@@ -23,6 +24,7 @@ before(async () => {
|
||||
mkdirSync(join(staticDir, "assets"));
|
||||
writeFileSync(join(staticDir, "index.html"), `<!DOCTYPE html><html><body>${INDEX_MARKER}</body></html>`);
|
||||
writeFileSync(join(staticDir, "assets", "index-abc12345.js"), ASSET_BODY);
|
||||
writeFileSync(join(staticDir, "favicon.svg"), FAVICON_BODY);
|
||||
|
||||
process.env.NODE_ENV = "test";
|
||||
process.env.STATIC_DIR = staticDir;
|
||||
@@ -93,6 +95,32 @@ describe("Frontend serveren", () => {
|
||||
assert.equal(await res.text(), ASSET_BODY);
|
||||
assert.match(res.headers.get("cache-control") ?? "", /immutable/);
|
||||
});
|
||||
|
||||
it("favicon wordt geserveerd als svg", async () => {
|
||||
const res = await fetch(`${basis}/favicon.svg`);
|
||||
assert.equal(res.status, 200);
|
||||
assert.match(res.headers.get("content-type") ?? "", /image\/svg\+xml/);
|
||||
assert.equal(await res.text(), FAVICON_BODY);
|
||||
});
|
||||
|
||||
// De naam bevat geen hash, dus een jaar "immutable" zou een oude of
|
||||
// verkeerde versie onhoudbaar lang in de browsercache vastzetten.
|
||||
it("favicon wordt niet immutable gecached", async () => {
|
||||
const res = await fetch(`${basis}/favicon.svg`);
|
||||
const cache = res.headers.get("cache-control") ?? "";
|
||||
assert.doesNotMatch(cache, /immutable/);
|
||||
assert.match(cache, /must-revalidate/);
|
||||
});
|
||||
|
||||
// Anders krijgt de browser index.html met status 200 en content-type
|
||||
// text/html waar hij een icoon verwacht, en toont hij stil niets.
|
||||
it("ontbrekend bestand geeft 404 in plaats van index.html", async () => {
|
||||
for (const pad of ["/favicon.ico", "/apple-touch-icon.png"]) {
|
||||
const res = await fetch(`${basis}${pad}`);
|
||||
assert.equal(res.status, 404, pad);
|
||||
assert.doesNotMatch(await res.text(), new RegExp(INDEX_MARKER), pad);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Security headers", () => {
|
||||
|
||||
Reference in New Issue
Block a user