From 0635daa742534dfb3b5994825f14cef19eee354c Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Sun, 22 Mar 2026 20:43:01 +0100 Subject: [PATCH] Remove env endpoint and mark live APIs no-store --- nginx.conf | 19 ------------------- src/core/configuration/ConfigLoader.ts | 15 +++------------ src/server/GamePreviewRoute.ts | 2 ++ src/server/Master.ts | 14 ++++++-------- src/server/NoStoreHeaders.ts | 10 ++++++++++ src/server/RenderHtml.ts | 8 ++------ src/server/Worker.ts | 6 ++++++ tests/server/NoStoreHeaders.test.ts | 21 +++++++++++++++++++++ 8 files changed, 50 insertions(+), 45 deletions(-) create mode 100644 src/server/NoStoreHeaders.ts create mode 100644 tests/server/NoStoreHeaders.test.ts diff --git a/nginx.conf b/nginx.conf index 081481739..fbba374f4 100644 --- a/nginx.conf +++ b/nginx.conf @@ -195,25 +195,6 @@ server { add_header Cache-Control "public, max-age=86400"; # 24 hours } - # /api/env endpoint - Cache for 1 hour - location = /api/env { - proxy_pass http://127.0.0.1:3000; - proxy_http_version 1.1; - - # Cache configuration - proxy_cache API_CACHE; - proxy_cache_valid 200 1h; # Cache successful responses for 1 hour - proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504; - proxy_cache_lock on; - add_header X-Cache-Status $upstream_cache_status; - - # Standard proxy headers - 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; - } - # /api/health endpoint - No caching, always hit the backend location = /api/health { proxy_pass http://127.0.0.1:3000; diff --git a/src/core/configuration/ConfigLoader.ts b/src/core/configuration/ConfigLoader.ts index 0b8c79c8a..4987da1c3 100644 --- a/src/core/configuration/ConfigLoader.ts +++ b/src/core/configuration/ConfigLoader.ts @@ -40,20 +40,11 @@ export async function getServerConfigFromClient(): Promise { } const bootstrapGameEnv = window.BOOTSTRAP_CONFIG?.gameEnv; - if (bootstrapGameEnv) { - cachedSC = getServerConfig(bootstrapGameEnv); - return cachedSC; + if (!bootstrapGameEnv) { + throw new Error("Missing bootstrap server config"); } - const response = await fetch("/api/env"); - if (!response.ok) { - throw new Error( - `Failed to fetch server config: ${response.status} ${response.statusText}`, - ); - } - const config = await response.json(); - - cachedSC = getServerConfig(config.game_env); + cachedSC = getServerConfig(bootstrapGameEnv); return cachedSC; } export function getServerConfigFromServer(): ServerConfig { diff --git a/src/server/GamePreviewRoute.ts b/src/server/GamePreviewRoute.ts index 9cf28f263..b2abe2b36 100644 --- a/src/server/GamePreviewRoute.ts +++ b/src/server/GamePreviewRoute.ts @@ -14,6 +14,7 @@ import { ExternalGameInfo, ExternalGameInfoSchema, } from "./GamePreviewBuilder"; +import { setNoStoreHeaders } from "./NoStoreHeaders"; import { renderHtmlContent, setHtmlNoCacheHeaders } from "./RenderHtml"; const requestOrigin = (req: Request, config: ServerConfig): string => { @@ -151,6 +152,7 @@ export function registerGamePreviewRoute(opts: { } // Fallback to JSON if HTML file not found + setNoStoreHeaders(res); res.setHeader("Content-Type", "application/json"); return res.send(JSON.stringify(lobby ?? publicInfo, replacer)); } catch (error) { diff --git a/src/server/Master.ts b/src/server/Master.ts index 7592ed25a..1adbec277 100644 --- a/src/server/Master.ts +++ b/src/server/Master.ts @@ -10,6 +10,7 @@ import { getServerConfigFromServer } from "../core/configuration/ConfigLoader"; import { logger } from "./Logger"; import { MapPlaylist } from "./MapPlaylist"; import { MasterLobbyService } from "./MasterLobbyService"; +import { setNoStoreHeaders } from "./NoStoreHeaders"; import { renderHtml } from "./RenderHtml"; import { applyStaticAssetCacheControl } from "./StaticAssetCache"; @@ -61,6 +62,11 @@ app.use( }), ); +app.use("/api", (_req, res, next) => { + setNoStoreHeaders(res); + next(); +}); + // Start the master process export async function startMaster() { if (!cluster.isPrimary) { @@ -133,14 +139,6 @@ export async function startMaster() { }); } -app.get("/api/env", async (req, res) => { - const envConfig = { - game_env: process.env.GAME_ENV, - }; - if (!envConfig.game_env) return res.sendStatus(500); - res.json(envConfig); -}); - app.get("/api/health", (_req, res) => { const ready = lobbyService?.isHealthy() ?? false; if (ready) { diff --git a/src/server/NoStoreHeaders.ts b/src/server/NoStoreHeaders.ts new file mode 100644 index 000000000..77996c025 --- /dev/null +++ b/src/server/NoStoreHeaders.ts @@ -0,0 +1,10 @@ +import type { Response } from "express"; + +export function setNoStoreHeaders(res: Response): void { + res.setHeader( + "Cache-Control", + "no-store, no-cache, must-revalidate, proxy-revalidate", + ); + res.setHeader("Pragma", "no-cache"); + res.setHeader("Expires", "0"); +} diff --git a/src/server/RenderHtml.ts b/src/server/RenderHtml.ts index 9c31b2721..58ca4119a 100644 --- a/src/server/RenderHtml.ts +++ b/src/server/RenderHtml.ts @@ -2,6 +2,7 @@ import ejs from "ejs"; import type { Response } from "express"; import fs from "fs/promises"; import { buildAssetUrl } from "../core/AssetUrls"; +import { setNoStoreHeaders } from "./NoStoreHeaders"; import { getRuntimeAssetManifest } from "./RuntimeAssetManifest"; export async function renderHtmlContent(htmlPath: string): Promise { @@ -25,12 +26,7 @@ export async function renderHtmlContent(htmlPath: string): Promise { } export function setHtmlNoCacheHeaders(res: Response): void { - res.setHeader( - "Cache-Control", - "no-store, no-cache, must-revalidate, proxy-revalidate", - ); - res.setHeader("Pragma", "no-cache"); - res.setHeader("Expires", "0"); + setNoStoreHeaders(res); res.setHeader("ETag", ""); res.setHeader("Content-Type", "text/html"); } diff --git a/src/server/Worker.ts b/src/server/Worker.ts index 1ec8c0281..98e1aa3e2 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -27,6 +27,7 @@ import { logger } from "./Logger"; import { GameEnv } from "../core/configuration/Config"; import { MapPlaylist } from "./MapPlaylist"; +import { setNoStoreHeaders } from "./NoStoreHeaders"; import { startPolling } from "./PollingLoop"; import { PrivilegeRefresher } from "./PrivilegeRefresher"; import { applyStaticAssetCacheControl } from "./StaticAssetCache"; @@ -139,6 +140,11 @@ export async function startWorker() { }), ); + app.use("/api", (_req, res, next) => { + setNoStoreHeaders(res); + next(); + }); + app.post("/api/create_game/:id", async (req, res) => { const id = req.params.id; diff --git a/tests/server/NoStoreHeaders.test.ts b/tests/server/NoStoreHeaders.test.ts new file mode 100644 index 000000000..a25234e16 --- /dev/null +++ b/tests/server/NoStoreHeaders.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from "vitest"; +import { setNoStoreHeaders } from "../../src/server/NoStoreHeaders"; + +describe("NoStoreHeaders", () => { + test("sets explicit no-store headers", () => { + const headers = new Map(); + const response = { + setHeader(name: string, value: string) { + headers.set(name, value); + }, + } as any; + + setNoStoreHeaders(response); + + expect(headers.get("Cache-Control")).toBe( + "no-store, no-cache, must-revalidate, proxy-revalidate", + ); + expect(headers.get("Pragma")).toBe("no-cache"); + expect(headers.get("Expires")).toBe("0"); + }); +});