diff --git a/src/client/api/status.ts b/src/client/api/status.ts index d789119..db1e635 100644 --- a/src/client/api/status.ts +++ b/src/client/api/status.ts @@ -40,13 +40,13 @@ export type PublicStatus = { }; export function getStatus(signal?: AbortSignal) { - return getJson('/api/status', { signal }); + return getJson('/api/status', { signal, cache: 'no-store' }); } export function getIncidentHistory(signal?: AbortSignal) { - return getJson<{ incidents: PublicIncident[] }>('/api/status/incidents', { signal }); + return getJson<{ incidents: PublicIncident[] }>('/api/status/incidents', { signal, cache: 'no-store' }); } export function getPublicIncident(id: number, signal?: AbortSignal) { - return getJson<{ incident: PublicIncident }>(`/api/status/incidents/${id}`, { signal }); + return getJson<{ incident: PublicIncident }>(`/api/status/incidents/${id}`, { signal, cache: 'no-store' }); } diff --git a/src/client/queries/status.ts b/src/client/queries/status.ts index 8dafcbd..5c8d142 100644 --- a/src/client/queries/status.ts +++ b/src/client/queries/status.ts @@ -12,7 +12,7 @@ export function useStatusQuery() { queryKey: statusKeys.all, queryFn: ({ signal }) => getStatus(signal), refetchInterval: 60_000, - refetchIntervalInBackground: true, + refetchIntervalInBackground: false, refetchOnWindowFocus: true, }); } @@ -21,8 +21,9 @@ export function useIncidentHistoryQuery() { return useQuery({ queryKey: statusKeys.history, queryFn: ({ signal }) => getIncidentHistory(signal), - refetchInterval: 120_000, + refetchInterval: 300_000, refetchIntervalInBackground: false, + refetchOnWindowFocus: true, }); } diff --git a/src/worker/routes/status.ts b/src/worker/routes/status.ts index 2506934..342a1cb 100644 --- a/src/worker/routes/status.ts +++ b/src/worker/routes/status.ts @@ -10,6 +10,11 @@ import { resolveFavicon } from './monitors'; const DAY_MS = 24 * 60 * 60 * 1000; const FAVICON_CACHE_SECONDS = 86_400; const STATUS_CACHE_TIME_HEADER = 'X-Upwatch-Status-Cache-Time'; +const STATUS_NO_STORE_HEADERS = { + 'Cache-Control': 'no-store', + 'CDN-Cache-Control': 'no-store', + 'Cloudflare-CDN-Cache-Control': 'no-store', +}; type ServiceStatus = 'up' | 'degraded' | 'down' | 'unknown' | 'maintenance'; type OverallStatus = 'operational' | 'degraded' | 'down'; type DailyAggregate = { monitorId: number; day: Date; totalChecks: number; upChecks: number }; @@ -143,7 +148,7 @@ async function cachedStatusResponse(context: Context<{ Bindings: Env }>): Promis const cached = await cache.match(statusCacheKey(context)); if (!cached) return undefined; if (!isStatusCacheFresh(cached, maxAgeSeconds)) return undefined; - return cached; + return statusResponse(cached); } catch { return undefined; } @@ -154,10 +159,21 @@ export function isStatusCacheFresh(response: Response, maxAgeSeconds: number, no return Number.isFinite(cachedAt) && now - cachedAt < maxAgeSeconds * 1_000; } +function statusResponse(response: Response): Response { + const headers = new Headers(response.headers); + for (const [name, value] of Object.entries(STATUS_NO_STORE_HEADERS)) headers.set(name, value); + headers.delete(STATUS_CACHE_TIME_HEADER); + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }); +} + function jsonWithEdgeCache(context: Context<{ Bindings: Env }>, body: unknown): Response { const seconds = resolveStatusCacheSeconds(context.env); - if (seconds <= 0) return Response.json(body); - const response = Response.json(body, { + if (seconds <= 0) return Response.json(body, { headers: STATUS_NO_STORE_HEADERS }); + const cachedResponse = Response.json(body, { headers: { 'Cache-Control': `public, max-age=${seconds}`, [STATUS_CACHE_TIME_HEADER]: String(Date.now()), @@ -166,12 +182,12 @@ function jsonWithEdgeCache(context: Context<{ Bindings: Env }>, body: unknown): const cache = edgeCache(); if (cache) { try { - context.executionCtx.waitUntil(cache.put(statusCacheKey(context), response.clone()).catch(() => undefined)); + context.executionCtx.waitUntil(cache.put(statusCacheKey(context), cachedResponse).catch(() => undefined)); } catch { // No ExecutionContext available (e.g. unit tests): serve without populating the edge cache. } } - return response; + return Response.json(body, { headers: STATUS_NO_STORE_HEADERS }); } const statusRoutes = new Hono<{ Bindings: Env }>(); diff --git a/test/status.spec.ts b/test/status.spec.ts index 1dfe346..0f49ee6 100644 --- a/test/status.spec.ts +++ b/test/status.spec.ts @@ -119,6 +119,10 @@ describe('public status API', () => { const body = await response.json(); expect(response.status).toBe(200); + expect(response.headers.get('Cache-Control')).toBe('no-store'); + expect(response.headers.get('CDN-Cache-Control')).toBe('no-store'); + expect(response.headers.get('Cloudflare-CDN-Cache-Control')).toBe('no-store'); + expect(response.headers.get('X-Upwatch-Status-Cache-Time')).toBeNull(); expect(body.overall).toBe('operational'); expect(body.updatedAt).toEqual(expect.any(Number)); expect(body.services).toHaveLength(1);