Make immutable asset cache headers explicit

This commit is contained in:
scamiv
2026-03-22 20:38:43 +01:00
parent 3d73f182b2
commit e003ffc468
5 changed files with 164 additions and 11 deletions
+93
View File
@@ -17,6 +17,99 @@ server {
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
location ^~ /assets/ {
proxy_pass http://127.0.0.1:3000;
proxy_cache STATIC;
proxy_cache_valid 200 302 24h;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status;
add_header Cache-Control "public, max-age=31536000, immutable";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ^~ /_assets/ {
proxy_pass http://127.0.0.1:3000;
proxy_cache STATIC;
proxy_cache_valid 200 302 24h;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status;
add_header Cache-Control "public, max-age=31536000, immutable";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~* ^/w(\d+)(/(?:assets|_assets)/.*)$ {
set $worker $1;
set $worker_port 3001;
if ($worker = "0") { set $worker_port 3001; }
if ($worker = "1") { set $worker_port 3002; }
if ($worker = "2") { set $worker_port 3003; }
if ($worker = "3") { set $worker_port 3004; }
if ($worker = "4") { set $worker_port 3005; }
if ($worker = "5") { set $worker_port 3006; }
if ($worker = "6") { set $worker_port 3007; }
if ($worker = "7") { set $worker_port 3008; }
if ($worker = "8") { set $worker_port 3009; }
if ($worker = "9") { set $worker_port 3010; }
if ($worker = "10") { set $worker_port 3011; }
if ($worker = "11") { set $worker_port 3012; }
if ($worker = "12") { set $worker_port 3013; }
if ($worker = "13") { set $worker_port 3014; }
if ($worker = "14") { set $worker_port 3015; }
if ($worker = "15") { set $worker_port 3016; }
if ($worker = "16") { set $worker_port 3017; }
if ($worker = "17") { set $worker_port 3018; }
if ($worker = "18") { set $worker_port 3019; }
if ($worker = "19") { set $worker_port 3020; }
if ($worker = "20") { set $worker_port 3021; }
if ($worker = "21") { set $worker_port 3022; }
if ($worker = "22") { set $worker_port 3023; }
if ($worker = "23") { set $worker_port 3024; }
if ($worker = "24") { set $worker_port 3025; }
if ($worker = "25") { set $worker_port 3026; }
if ($worker = "26") { set $worker_port 3027; }
if ($worker = "27") { set $worker_port 3028; }
if ($worker = "28") { set $worker_port 3029; }
if ($worker = "29") { set $worker_port 3030; }
if ($worker = "30") { set $worker_port 3031; }
if ($worker = "31") { set $worker_port 3032; }
if ($worker = "32") { set $worker_port 3033; }
if ($worker = "33") { set $worker_port 3034; }
if ($worker = "34") { set $worker_port 3035; }
if ($worker = "35") { set $worker_port 3036; }
if ($worker = "36") { set $worker_port 3037; }
if ($worker = "37") { set $worker_port 3038; }
if ($worker = "38") { set $worker_port 3039; }
if ($worker = "39") { set $worker_port 3040; }
if ($worker = "40") { set $worker_port 3041; }
proxy_pass http://127.0.0.1:$worker_port$2$is_args$args;
proxy_cache STATIC;
proxy_cache_valid 200 302 24h;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status;
add_header Cache-Control "public, max-age=31536000, immutable";
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Worker locations - Processing this first so worker-specific requests are handled by workers
# This prevents static file regexes from capturing worker requests
location ~* ^/w(\d+)(/.*)?$ {
+6 -10
View File
@@ -11,6 +11,7 @@ import { logger } from "./Logger";
import { MapPlaylist } from "./MapPlaylist";
import { MasterLobbyService } from "./MasterLobbyService";
import { renderHtml } from "./RenderHtml";
import { applyStaticAssetCacheControl } from "./StaticAssetCache";
const config = getServerConfigFromServer();
const playlist = new MapPlaylist();
@@ -43,16 +44,11 @@ app.use(async (req, res, next) => {
app.use(
express.static(path.join(__dirname, "../../static"), {
maxAge: "1y", // Set max-age to 1 year for all static assets
setHeaders: (res, path) => {
// You can conditionally set different cache times based on file types
if (path.match(/\.(js|css|svg)$/)) {
// JS, CSS, SVG get long cache with immutable
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
} else if (path.match(/\.(bin|dat|exe|dll|so|dylib)$/)) {
// Binary files also get long cache with immutable
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
}
// Other file types use the default maxAge setting
setHeaders: (res) => {
applyStaticAssetCacheControl(
res.setHeader.bind(res),
res.req.originalUrl,
);
},
}),
);
+33
View File
@@ -0,0 +1,33 @@
const IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
function stripQueryString(urlPath: string): string {
return urlPath.split("?", 1)[0];
}
export function getStaticAssetCacheControl(
urlPath: string | undefined,
): string | undefined {
if (!urlPath) {
return undefined;
}
const normalizedPath = stripQueryString(urlPath);
if (
normalizedPath.startsWith("/assets/") ||
normalizedPath.startsWith("/_assets/")
) {
return IMMUTABLE_CACHE_CONTROL;
}
return undefined;
}
export function applyStaticAssetCacheControl(
setHeader: (name: string, value: string) => void,
urlPath: string | undefined,
): void {
const cacheControl = getStaticAssetCacheControl(urlPath);
if (cacheControl) {
setHeader("Cache-Control", cacheControl);
}
}
+11 -1
View File
@@ -29,6 +29,7 @@ import { GameEnv } from "../core/configuration/Config";
import { MapPlaylist } from "./MapPlaylist";
import { startPolling } from "./PollingLoop";
import { PrivilegeRefresher } from "./PrivilegeRefresher";
import { applyStaticAssetCacheControl } from "./StaticAssetCache";
import { verifyTurnstileToken } from "./Turnstile";
import { WorkerLobbyService } from "./WorkerLobbyService";
import { initWorkerMetrics } from "./WorkerMetrics";
@@ -110,7 +111,16 @@ export async function startWorker() {
// Configure MIME types for webp files
express.static.mime.define({ "image/webp": ["webp"] });
app.use(express.static(path.join(__dirname, "../../out")));
app.use(
express.static(path.join(__dirname, "../../out"), {
setHeaders: (res) => {
applyStaticAssetCacheControl(
res.setHeader.bind(res),
res.req.originalUrl,
);
},
}),
);
app.use(
"/maps",
express.static(path.join(__dirname, "../../static/maps"), {
+21
View File
@@ -0,0 +1,21 @@
import { describe, expect, test } from "vitest";
import { getStaticAssetCacheControl } from "../../src/server/StaticAssetCache";
describe("StaticAssetCache", () => {
test("marks Vite asset namespace as immutable", () => {
expect(getStaticAssetCacheControl("/assets/index-abc123.js")).toBe(
"public, max-age=31536000, immutable",
);
});
test("marks custom hashed asset namespace as immutable", () => {
expect(
getStaticAssetCacheControl("/_assets/maps/world/manifest.hash.json"),
).toBe("public, max-age=31536000, immutable");
});
test("does not mark other paths as immutable", () => {
expect(getStaticAssetCacheControl("/manifest.json")).toBeUndefined();
expect(getStaticAssetCacheControl("/api/health")).toBeUndefined();
});
});