Public: anyone can read this; the crew can change it.
GET /api/v1/admin/capacity, /admin/limits and /admin/quotas carry an ETag and answer a repeat with 304 until a limit or the capacity reading changes #9266
Check · Review · Land · Live done — Landed and live in production.
`GET /api/v1/admin/capacity`, `/admin/limits` and `/admin/quotas` now return a weak ETag and answer a repeat request with 304 until a limit, a quota or a capacity reading changes.
shipped production is serving it deploy 0b2906d
GET /api/v1/admin/capacity, /admin/limits and /admin/quotas now return a weak ETag and answer a repeat request with 304 until a limit, a quota or a capacity reading changes.
- What changed: in
src/http/api/admin.ts, the three GET routes now build their response withtaggedJsoninstead ofjson.taggedJsonalready sat beside them (it serves/admin/peopleand the other lists). It computes the weak tag from the exact bytes sent and setscache-control: private, no-cacheandvary: authorization, cookie. It answers 304 with no body whenIf-None-Matchmatches. - Order of checks:
taggedJsonruns only afteraccountAdminand the "not an agent" check. A refused request (no session, a member, an agent admin) answers with the same status and body as before and no ETag, even when it sendsIf-None-Match: *. - Bodies: unchanged byte for byte, because both helpers use the same
JSON.stringify. - Untouched:
src/http/api.tsand every other admin route.
Tests: test/admin-limits-etag.test.ts checks four cases on each of the three routes:
- The 200 response carries the tag and headers, and the 304 repeat keeps the headers, has no body and keeps the
securityFloorheaders. - A PUT to
checks.concurrency,share.daily_maxorpreviewschanges the tag, and resetting it changes it again. - A second admin who sees the same bytes gets a 304 with the same tag, so the tag depends on the body, not the viewer.
- Refused callers answer as before with no ETag.
Without the change, the first three cases fail on every route (9 of 12 tests). The refused-caller case passes either way: it describes how these routes already behave, and only fails if the 304 is ever answered before the access check.
Not covered: the issue asks for "a token short of each route's scope", but no such token exists for these reads. They need ro, the lowest scope, so the refused cases test the callers that actually get turned away. Separately, the existing capacity tests show a member being refused with 403, while accountAdmin would return 404. The refused-caller test compares against each route's current answer rather than hard-coding a status.
Checks run: bun scripts/gate.ts, bun x tsc --noEmit and the new suite pass, as do the quotas, capacity, limits, admin-gate, admin-people-etag and admin timing suites. To try it, curl -i one of the routes with an admin token, then repeat with If-None-Match: <etag> to get the 304.
🤖 Generated with Claude Code
GET /api/v1/admin/capacity, /admin/limits and /admin/quotas now send a weak ETag and answer a repeat request with a 304 until a limit, a quota or a capacity reading changes.
- The change: in
src/http/api/admin.ts, all three GET routes build their response with the existingtaggedJsonhelper instead ofjson. That helper already serves/admin/peopleand the other admin lists. - Headers: the tag is computed from the exact body sent, with
cache-control: private, no-cacheandvary: authorization, cookie. - Order of checks: the 304 is answered only after the access check. A request that is refused (no session, a member, an agent admin) gets the same status and body as before and no ETag, even with
If-None-Match: *. - Bodies: unchanged byte for byte, because both helpers use the same
JSON.stringify. - Untouched:
src/http/api.tsand every other admin route.
Tests: test/admin-limits-etag.test.ts checks four things on each of the three routes:
- The first answer is a 200 with the tag and headers, and a repeat is a 304 with no body that keeps the headers and the
securityFloorheaders. - A PUT to
checks.concurrency,share.daily_maxorpreviewschanges the tag, and resetting it changes it again. - A second admin shown the same body gets a 304 with the same tag, so two viewers share a tag only when they see the same body.
- Refused callers answer as before, with no ETag.
Without the change, the first three cases fail on every route (9 of 12 tests). The refused-caller case passes either way: it guards that the 304 never comes before the access check.
What I didn't cover: the issue asks for "a token short of each route's scope". These reads need ro, which is already the lowest scope, so no token falls short; the refused cases use the callers that are actually turned away. That test also compares against each route's current answer instead of fixing a status, because the existing capacity tests expect 403 for a member where accountAdmin alone would give 404.
Checks: the gate, bun x tsc --noEmit, the new suite and the neighbouring quotas, capacity, limits, admin-gate, admin-people-etag and admin timing suites passed on the change. The second commit (no code change) adds the Try: line the preview gate needs; I didn't re-run the tests after adding it.
Try: on the preview, as an admin, call GET /api/v1/admin/limits (or /admin/capacity, /admin/quotas) with a token. Repeat with If-None-Match set to the returned etag and you get a 304. After PUT /api/v1/admin/limits/share.daily_max, the same tag gets a 200 with a new etag.
🤖 Generated with Claude Code
Clean, minimal: the three GETs use the existing taggedJson after accountAdmin, so refused callers get no ETag and the 304 only follows the access check. Bodies unchanged. Tests cover 200/304 with the floor headers, tag moving on a PUT and on reset, shared tag across admins, and refused callers. No terminal or README change needed (cache headers on existing reads). Box 4 gap: no token can fall short of ro, so that part is vacuous; accepted.
GET /api/v1/admin/capacity,/admin/limitsand/admin/quotasnow return a weak ETag and answer a repeat request with 304 until a limit, a quota or a capacity reading changes.src/http/api/admin.ts, the three GET routes now build their response withtaggedJsoninstead ofjson.taggedJsonalready sat beside them (it serves/admin/peopleand the other lists). It computes the weak tag from the exact bytes sent and setscache-control: private, no-cacheandvary: authorization, cookie. It answers 304 with no body whenIf-None-Matchmatches.taggedJsonruns only afteraccountAdminand the "not an agent" check. A refused request (no session, a member, an agent admin) answers with the same status and body as before and no ETag, even when it sendsIf-None-Match: *.JSON.stringify.src/http/api.tsand every other admin route.Tests:
test/admin-limits-etag.test.tschecks four cases on each of the three routes:securityFloorheaders.checks.concurrency,share.daily_maxorpreviewschanges the tag, and resetting it changes it again.Without the change, the first three cases fail on every route (9 of 12 tests). The refused-caller case passes either way: it describes how these routes already behave, and only fails if the 304 is ever answered before the access check.
Not covered: the issue asks for "a token short of each route's scope", but no such token exists for these reads. They need
ro, the lowest scope, so the refused cases test the callers that actually get turned away. Separately, the existing capacity tests show a member being refused with 403, whileaccountAdminwould return 404. The refused-caller test compares against each route's current answer rather than hard-coding a status.Checks run:
bun scripts/gate.ts,bun x tsc --noEmitand the new suite pass, as do the quotas, capacity, limits, admin-gate, admin-people-etag and admin timing suites. To try it,curl -ione of the routes with an admin token, then repeat withIf-None-Match: <etag>to get the 304.🤖 Generated with Claude Code
Closes #9248