diff --git a/apps/status/src/index.ts b/apps/status/src/index.ts index a96e520..7e10cec 100644 --- a/apps/status/src/index.ts +++ b/apps/status/src/index.ts @@ -37,10 +37,39 @@ function clientIp(req: Request): string { || "unknown"; } +// 404s must NOT be cached by browsers or Cloudflare. The same URL frequently +// flips between "200 with data" and "404 not-found" — for example when an +// operator adds a password to a previously-public page, or when a slug +// changes, or when a monitor is removed. If Cloudflare cached a 404 (default +// behaviour for unspecified Cache-Control on 404 responses) the operator +// would see stale 404s for the entire edge TTL. Likewise, an old 200 cached +// at the edge could keep serving image/svg+xml headers over a now-changed +// body, which is exactly the symptom that prompted this fix. +// +// no-store is the bluntest form: never store, always revalidate. Pair with +// 404 status so any layer in front knows not to hold on to it. +const NO_STORE_HEADERS = { + "cache-control": "no-store, must-revalidate", + "pragma": "no-cache", +} as const; + function notFound(): Response { return new Response(eta.render("not-found", {}), { status: 404, - headers: { "content-type": "text/html; charset=utf-8" }, + headers: { + "content-type": "text/html; charset=utf-8", + ...NO_STORE_HEADERS, + }, + }); +} + +function jsonNotFound(): Response { + return new Response(JSON.stringify({ error: "not found" }), { + status: 404, + headers: { + "content-type": "application/json", + ...NO_STORE_HEADERS, + }, }); } @@ -92,12 +121,10 @@ async function renderJson(slug: string, request: Request, win?: Window): Promise // as an oracle for which slugs are private. Forces any OSINT enumeration to // fall back to HTML scraping (which gets the password form, also a 401-ish // signal but more expensive to parse and less stable). - if (!page || !isAuthorised(page, request)) { - return new Response(JSON.stringify({ error: "not found" }), { status: 404, headers: { "content-type": "application/json" } }); - } + if (!page || !isAuthorised(page, request)) return jsonNotFound(); const cacheKey = `payload:${slug}:${win ?? page.default_window}`; const payload = await cached(cacheKey, 15, () => loadPagePayload(slug, win)); - if (!payload) return new Response(JSON.stringify({ error: "not found" }), { status: 404, headers: { "content-type": "application/json" } }); + if (!payload) return jsonNotFound(); return new Response(JSON.stringify(payload), { headers: { "content-type": "application/json", @@ -190,29 +217,13 @@ const app = new Elysia() // monitor id (8 hex bytes). Match the page existence 404 so unauthenticated // requests can't even tell whether a slug is gated. const page = await loadStatusPage(params.slug); - if (!page) { - return new Response(JSON.stringify({ error: "not found" }), { - status: 404, - headers: { "content-type": "application/json" }, - }); - } - if (!isAuthorised(page, request)) { - return new Response(JSON.stringify({ error: "not found" }), { - status: 404, - headers: { "content-type": "application/json" }, - }); - } + if (!page || !isAuthorised(page, request)) return jsonNotFound(); const idWithExt = params.idWithExt; const monitorId = idWithExt.endsWith(".json") ? idWithExt.slice(0, -5) : idWithExt; const win = (query as any)?.window as Window | undefined; const cacheKey = `monitor:${params.slug}:${monitorId}:${win ?? ''}`; const payload = await cached(cacheKey, 15, () => loadMonitorDetail(params.slug, monitorId, win)); - if (!payload) { - return new Response(JSON.stringify({ error: "not found" }), { - status: 404, - headers: { "content-type": "application/json" }, - }); - } + if (!payload) return jsonNotFound(); return new Response(JSON.stringify(payload), { headers: { "content-type": "application/json",