From 6dba22ab0c7663a192c5a2141ca06a7a7973286c Mon Sep 17 00:00:00 2001 From: Manas Srivastava Date: Sat, 30 May 2026 14:05:03 +0530 Subject: [PATCH] fix(api): /api/v1/capabilities Cache-Control + must-revalidate (BUG-API-039/311) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /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) --- internal/handlers/capabilities.go | 17 +++++++++++++++++ internal/handlers/capabilities_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/internal/handlers/capabilities.go b/internal/handlers/capabilities.go index 3af82735..8bfdec72 100644 --- a/internal/handlers/capabilities.go +++ b/internal/handlers/capabilities.go @@ -191,6 +191,23 @@ func (h *CapabilitiesHandler) Get(c *fiber.Ctx) error { }) } + // BUG-API-039 / BUG-API-311 (QA 2026-05-29): /api/v1/capabilities + // is dashboard-hit on every nav (sidebar tile counts, billing card, + // settings page render). The response only changes when + // api/plans.yaml is edited and the binary is redeployed — so the + // tier matrix is *immutable for the life of the running pod*. + // Without a Cache-Control hint each dashboard nav re-fetched the + // full ~4 KB matrix; sidebar fanout meant 4-6 redundant fetches per + // nav (BUG-DASH-016 noise). A `max-age=60` directive lets the + // browser fetch cache + intermediaries serve the response from the + // edge for a minute, cutting tile-render latency without hiding a + // real tier change for longer than one rule-23 deploy cycle (the + // build-SHA flip invalidates the proxy cache via /healthz polling). + // `public` because the tier matrix is the same for every caller + // (no per-user discrimination); `must-revalidate` so stale-while- + // revalidate proxies still re-fetch on expiry rather than serving + // indefinitely-stale rows after an extended offline period. + c.Set(fiber.HeaderCacheControl, "public, max-age=60, must-revalidate") return c.JSON(fiber.Map{ "ok": true, "tiers": out, diff --git a/internal/handlers/capabilities_test.go b/internal/handlers/capabilities_test.go index 66704653..3efc62aa 100644 --- a/internal/handlers/capabilities_test.go +++ b/internal/handlers/capabilities_test.go @@ -283,6 +283,30 @@ func TestCapabilities_TerminalTierUpgradeURLIsNull(t *testing.T) { } } +// TestCapabilities_CacheControlPublicMaxAge60 pins BUG-API-039 / +// BUG-API-311: /api/v1/capabilities is dashboard-hit on every nav +// (sidebar tiles, billing card, settings) and the tier matrix is +// immutable for the life of the running pod (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 (BUG-DASH-016). +// +// Pin `public, max-age=60, must-revalidate` so the browser/edge cache +// serves the matrix for a minute while still re-validating on expiry. +// A future deletion of the c.Set call fails this assertion before merge. +func TestCapabilities_CacheControlPublicMaxAge60(t *testing.T) { + reg := plans.Default() + app := newCapabilitiesApp(t, reg) + req := httptest.NewRequest(http.MethodGet, "/api/v1/capabilities", nil) + resp, err := app.Test(req, -1) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, "public, max-age=60, must-revalidate", + resp.Header.Get("Cache-Control"), + "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") +} + // TestCapabilities_AnnualDiscountFromYAML — when a {tier}_yearly variant // exists in the registry, the canonical tier reports a non-zero // annual_discount_percent computed from (1 - yearly/(monthly*12)).