fix(health-freshness): read the keyless compact health endpoint — stop the anon 401-per-minute flood (#4902)#4905
Conversation
…p the anon 401-per-minute flood (#4902) #4715 key-gated detailed /api/health (operator/enterprise only) but the dashboard client kept fetching it bare: every anonymous tab 401'd once a minute and the seed-health → data-freshness pipeline went silently dead. - Default the client to /api/health?compact=1 (public). `problems` carries the same per-check shape; mapped checks absent from it were evaluated server-side and found within budget, so synthesize OK-as-of-checkedAt (seedAgeMin 0 — an age-less update would leave lastUpdate null and calculateStatus reports no_data). - 401/403 → 15-min suppression window: one visible error per window per tab instead of one per tick if the endpoint ever gets re-gated (same caller-sweep flood class as #4865). - REDIS_DOWN 503 body-first handling unchanged (short-circuits before the compact branch server-side). Verified against live prod: 24 mapped sources hydrate, stale gdeltIntel maps very_stale, absent checks read fresh. Fixes #4902 Claude-Session: https://claude.ai/code/session_01YKrVoDp8TfEKLYXo83kPFV
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR fixes a 401-per-minute flood caused by the anonymous dashboard calling the key-gated
Confidence Score: 4/5Safe to merge; the fix directly addresses a live 401 flood and the new compact-payload logic is well-tested and well-commented. The synthesis guard at line 148 does not exclude Redis-outage statuses, so a hypothetical REDIS_DOWN compact response with non-empty problems would bypass the early-return Redis path and incorrectly synthesize OK for absent mapped sources. The server currently guarantees no problems alongside REDIS_DOWN, but the guard is easy to harden. No other logic concerns found; the 401-suppression and endpoint-switch paths are correct and covered by tests. src/services/health-freshness.ts — specifically the compact-synthesis guard at line 148. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[refreshDataFreshnessFromHealth] --> B{suppressed?}
B -- Yes --> C[return 0]
B -- No --> D[fetch /api/health?compact=1]
D --> E{status 401 or 403?}
E -- Yes --> F[set suppress window 15min\nthrow error]
E -- No --> G[parse JSON body]
G --> H{resp.ok or Redis outage?}
H -- No --> I[throw fetch error]
H -- Yes --> J{payload.checks present?}
J -- Yes: detailed --> K[checks = payload.checks]
J -- No: compact --> L[checks = spread of problems or empty]
L --> M{status is string?}
M -- Yes --> N[synthesize OK for absent\nHEALTH_CHECK_SOURCE_MAP keys]
M -- No --> O
K --> O{checks empty and Redis outage?}
N --> O
O -- Yes --> P[record all sources as REDIS_DOWN]
O -- No --> Q[apply highest-rank update per source]
P --> R[return count]
Q --> S[recordSeedHealth]
S --> R
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[refreshDataFreshnessFromHealth] --> B{suppressed?}
B -- Yes --> C[return 0]
B -- No --> D[fetch /api/health?compact=1]
D --> E{status 401 or 403?}
E -- Yes --> F[set suppress window 15min\nthrow error]
E -- No --> G[parse JSON body]
G --> H{resp.ok or Redis outage?}
H -- No --> I[throw fetch error]
H -- Yes --> J{payload.checks present?}
J -- Yes: detailed --> K[checks = payload.checks]
J -- No: compact --> L[checks = spread of problems or empty]
L --> M{status is string?}
M -- Yes --> N[synthesize OK for absent\nHEALTH_CHECK_SOURCE_MAP keys]
M -- No --> O
K --> O{checks empty and Redis outage?}
N --> O
O -- Yes --> P[record all sources as REDIS_DOWN]
O -- No --> Q[apply highest-rank update per source]
P --> R[return count]
Q --> S[recordSeedHealth]
S --> R
Reviews (1): Last reviewed commit: "fix(health-freshness): read the keyless ..." | Re-trigger Greptile |
| // and found within budget. Synthesize OK-as-of-checkedAt for those: | ||
| // seedAgeMin 0 is required because recordSeedHealth keeps lastUpdate null | ||
| // on an age-less update and calculateStatus then reports no_data. | ||
| if (!payload.checks && typeof payload.status === 'string') { |
There was a problem hiding this comment.
The synthesis guard
!payload.checks && typeof payload.status === 'string' does not exclude Redis-outage statuses. If the server ever returns a compact REDIS_DOWN/REDIS_PARTIAL payload with non-empty problems (e.g., a future change to the outage branch), the code would skip the early-return Redis path (because checks is non-empty), then synthesize OK for every absent mapped key alongside the genuinely broken sources — leaving most sources falsely marked fresh during a total Redis outage. Adding !isRedisOutageStatus(payload.status) closes this gap defensively.
| if (!payload.checks && typeof payload.status === 'string') { | |
| if (!payload.checks && typeof payload.status === 'string' && !isRedisOutageStatus(payload.status)) { |
…, 5-min cadence, idle-deferred first hydration (#4907) (#4908) Every /api/health call runs a ~196-key Upstash pipeline and responses were no-store end to end, so origin probe cost scaled linearly with open tabs (60s x per-tab x all variants) and the first fetch competed with the LCP window (#4890). - api/health.js: compact 200s (the public keyless form) get Cache-Control: public, max-age=0, must-revalidate + CDN-Cache-Control: public, s-maxage=60 — all tabs collapse onto ~one origin probe per cache window per edge region. Key-gated detailed responses, 401s, and the REDIS_DOWN 503 keep no-store (caching a 401 pins an auth failure; caching a 503 masks recovery). - REFRESH_INTERVALS.healthFreshness 60s -> 5min: seed cadences are 6-24h and badge decay is client-computed; 5min stays safely under the 15min FRESH_THRESHOLD so synthesized-OK sources (age-less since #4905) cannot decay to stale between polls. - App.ts: health-freshness registration (and its immediate first run) deferred via scheduleAfterFirstPaint — freshness badges are below-the-fold decoration and must not compete with LCP requests. New tests: compact-200 cache headers + no-store pins for detailed/401/503 (handler-level, mocked Upstash pipeline); scheduler-shape pin updated to require the post-paint defer. Fixes #4907 Claude-Session: https://claude.ai/code/session_01YKrVoDp8TfEKLYXo83kPFV
Fixes #4902.
Problem
#4715 key-gated detailed
/api/health(operator/enterprise only) but never swept the dashboard caller:src/services/health-freshness.tskept fetching the bare endpoint anonymously. Since the gate deployed (2026-07-04), every anonymous tab 401s once a minute (REFRESH_INTERVALS.healthFreshness = 60s,runImmediately: true, all variants), each failure flows to Sentry, and the seed-health → data-freshness pipeline is silently dead —refreshDataFreshnessFromHealth()throws beforerecordSeedHealth(), so panel freshness badges never see STALE_SEED / REDIS_DOWN. Same caller-sweep flood class as #4865 (→ #4869); the #4856 sweep fixed only the advertised consumers of this endpoint.Fix
/api/health?compact=1(server already pins this variant as public —tests/health-redis-down-status.test.mjs).problemscarries the same per-check shape aschecksbut only non-OK entries; a mapped check absent from it was evaluated server-side and found within budget, so the client synthesizes OK-as-of-checkedAt(seedAgeMin: 0— an age-less OK would leavelastUpdatenull andcalculateStatusreportsno_data).checksparsing and the REDIS_DOWN 503 body-first branch are unchanged (REDIS_DOWN short-circuits server-side before the compact branch, still nochecks/problems).Verification
tests/data-freshness-health.test.mts(failing-first): default-endpoint pin, compact problems + synthesized-OK hydration, healthy-compact clears prior errors, 401 suppression window. 13/13 pass; existing detailed-payload tests untouched and green.gdeltIntelmaps tovery_stale, absent checks readfreshas ofcheckedAt.npm run typecheckclean; tiered pre-push gate green (unit incl. changed tests, edge bundle, boundaries).https://claude.ai/code/session_01YKrVoDp8TfEKLYXo83kPFV