Skip to content

Commit bc7cc64

Browse files
fix(api): /api/v1/capabilities Cache-Control + must-revalidate (BUG-API-039/311) (#193)
/api/v1/capabilities is dashboard-hit on every navigation (sidebar tile counts, billing card, settings page) but the tier matrix is immutable for the life of the running pod — it only changes on a plans.yaml edit + redeploy. Without a Cache-Control hint each nav re-fetched the full ~4 KB matrix; sidebar fanout meant 4-6 redundant fetches per nav (see BUG-DASH-016). Stamp `public, max-age=60, must-revalidate` so browser/edge caches serve the matrix for a minute while still re-validating on expiry. The rule-23 deploy cycle flips the /healthz commit_id which invalidates the proxy cache shortly after a plans.yaml change ships, so the 60s ceiling is well below the user-observable change window. Coverage block: Symptom: /api/v1/capabilities uncached, no Cache-Control (BUG-API-039) /api/v1/capabilities no Last-Modified (BUG-API-311) Enumeration: rg -F 'capabilities' internal/handlers/capabilities.go rg -F 'NewCapabilitiesHandler' internal/router/router.go (1 emit site — internal/handlers/capabilities.go Get) Sites found: 1 Sites touched: 1 Coverage test: TestCapabilities_CacheControlPublicMaxAge60 asserts the exact `public, max-age=60, must-revalidate` string on the 200 path so a future deletion fails before merge. Live verified: pending auto-deploy + `curl -I https://api.instanode.dev/api/v1/capabilities | grep -i cache-control` Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d25ea33 commit bc7cc64

2 files changed

Lines changed: 41 additions & 0 deletions

File tree

internal/handlers/capabilities.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,23 @@ func (h *CapabilitiesHandler) Get(c *fiber.Ctx) error {
191191
})
192192
}
193193

194+
// BUG-API-039 / BUG-API-311 (QA 2026-05-29): /api/v1/capabilities
195+
// is dashboard-hit on every nav (sidebar tile counts, billing card,
196+
// settings page render). The response only changes when
197+
// api/plans.yaml is edited and the binary is redeployed — so the
198+
// tier matrix is *immutable for the life of the running pod*.
199+
// Without a Cache-Control hint each dashboard nav re-fetched the
200+
// full ~4 KB matrix; sidebar fanout meant 4-6 redundant fetches per
201+
// nav (BUG-DASH-016 noise). A `max-age=60` directive lets the
202+
// browser fetch cache + intermediaries serve the response from the
203+
// edge for a minute, cutting tile-render latency without hiding a
204+
// real tier change for longer than one rule-23 deploy cycle (the
205+
// build-SHA flip invalidates the proxy cache via /healthz polling).
206+
// `public` because the tier matrix is the same for every caller
207+
// (no per-user discrimination); `must-revalidate` so stale-while-
208+
// revalidate proxies still re-fetch on expiry rather than serving
209+
// indefinitely-stale rows after an extended offline period.
210+
c.Set(fiber.HeaderCacheControl, "public, max-age=60, must-revalidate")
194211
return c.JSON(fiber.Map{
195212
"ok": true,
196213
"tiers": out,

internal/handlers/capabilities_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,30 @@ func TestCapabilities_TerminalTierUpgradeURLIsNull(t *testing.T) {
283283
}
284284
}
285285

286+
// TestCapabilities_CacheControlPublicMaxAge60 pins BUG-API-039 /
287+
// BUG-API-311: /api/v1/capabilities is dashboard-hit on every nav
288+
// (sidebar tiles, billing card, settings) and the tier matrix is
289+
// immutable for the life of the running pod (only changes on a
290+
// plans.yaml edit + redeploy). Without a Cache-Control hint each nav
291+
// re-fetched the full ~4 KB matrix; sidebar fanout meant 4-6 redundant
292+
// fetches per nav (BUG-DASH-016).
293+
//
294+
// Pin `public, max-age=60, must-revalidate` so the browser/edge cache
295+
// serves the matrix for a minute while still re-validating on expiry.
296+
// A future deletion of the c.Set call fails this assertion before merge.
297+
func TestCapabilities_CacheControlPublicMaxAge60(t *testing.T) {
298+
reg := plans.Default()
299+
app := newCapabilitiesApp(t, reg)
300+
req := httptest.NewRequest(http.MethodGet, "/api/v1/capabilities", nil)
301+
resp, err := app.Test(req, -1)
302+
require.NoError(t, err)
303+
defer resp.Body.Close()
304+
require.Equal(t, http.StatusOK, resp.StatusCode)
305+
require.Equal(t, "public, max-age=60, must-revalidate",
306+
resp.Header.Get("Cache-Control"),
307+
"BUG-API-039/311: /api/v1/capabilities must stamp Cache-Control: public, max-age=60, must-revalidate so dashboard nav fanout doesn't re-fetch the same immutable matrix every navigation")
308+
}
309+
286310
// TestCapabilities_AnnualDiscountFromYAML — when a {tier}_yearly variant
287311
// exists in the registry, the canonical tier reports a non-zero
288312
// annual_discount_percent computed from (1 - yearly/(monthly*12)).

0 commit comments

Comments
 (0)