Public: anyone can read this; the crew can change it.

GET /api/v1/flags carries an ETag and answers a repeat with 304 until a flag changes #9216

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`GET /api/v1/flags` now sends a weak `etag` over the exact bytes it returns, plus `cache-control: private, no-cache` and `vary: authorization, cookie`. A repeat that sends the tag back in `If-None-Match` gets a 304 with the same headers and no body. It follows the same pattern as the forks route in #9172.

shipped production is serving it deploy ad44386

opened this

GET /api/v1/flags now sends a weak etag over the exact bytes it returns, plus cache-control: private, no-cache and vary: authorization, cookie. A repeat that sends the tag back in If-None-Match gets a 304 with the same headers and no body. It follows the same pattern as the forks route in #9172.

  • Access check first: the tag is computed only after the session check (401) and the readFlags permission check (403). Those two answers are unchanged and carry no ETag, even with If-None-Match: *.
  • Body unchanged: it is still JSON.stringify({ flags }), byte for byte. Because the tag is taken from those bytes, turning a flag on or off changes it, and two viewers shown different bodies can't share one. src/http/api.ts is untouched.

test/flags-etag.test.ts covers:

  • the 200's tag (it equals weakEtag of the body), the cache headers and the unchanged body;
  • the 304 with no body and the securityFloor headers (x-content-type-options, content-security-policy, referrer-policy);
  • the tag moving when the flag is set to { forge: true } and then to { forge: false };
  • 401 and 403 with no ETag, both with * and when a non-admin replays an admin's tag.

Without the route change, the first three tests fail. The 401/403 test passes either way, because it pins down behaviour that already exists.

The flags tests (test/flags-etag.test.ts, test/api-flags.test.ts, test/flags-timing.test.ts), bun scripts/gate.ts and bun x tsc --noEmit all pass locally. The issue asked for a plan kept with keep_plan, but that tool wasn't available in this session, so the issue has no plan.

To try it: curl -i -H "authorization: Bearer $ADMIN_TOKEN" …/api/v1/flags, then repeat with -H 'if-none-match: <etag>' and expect a 304.

🤖 Generated with Claude Code

Closes #9214

commented

GET /api/v1/flags now sends a weak etag over the exact bytes it returns, plus cache-control: private, no-cache and vary: authorization, cookie. A repeat that sends the tag back in If-None-Match gets a 304 with the same headers and no body. It follows the same pattern as the forks route in #9172.

  • Access check first: the tag is computed only after the session check (401) and the readFlags permission check (403). Those two answers are unchanged and carry no ETag, even with If-None-Match: *.
  • Body unchanged: it is still JSON.stringify({ flags }), byte for byte. Because the tag is taken from those bytes, turning a flag on or off changes it, and two viewers shown different bodies can't share one. src/http/api.ts is untouched.

test/flags-etag.test.ts covers:

  • the 200's tag (it equals weakEtag of the body), the cache headers and the unchanged body;
  • the 304 with no body and the securityFloor headers (x-content-type-options, content-security-policy, referrer-policy);
  • the tag moving when the flag is set to { forge: true } and then to { forge: false };
  • 401 and 403 with no ETag, both with * and when a non-admin replays an admin's tag.

Without the route change, the first three tests fail. The 401/403 test passes either way, because it pins down behaviour that already exists.

The flags tests (test/flags-etag.test.ts, test/api-flags.test.ts, test/flags-timing.test.ts), bun scripts/gate.ts and bun x tsc --noEmit all pass locally. The issue asked for a plan kept with keep_plan, but that tool wasn't available in this session, so the issue has no plan.

To try it: curl -i -H "authorization: Bearer $ADMIN_TOKEN" …/api/v1/flags, then repeat with -H 'if-none-match: <etag>' and expect a 304.

🤖 Generated with Claude Code

commented

GET /api/v1/flags now sends a weak etag, cache-control: private, no-cache and vary: authorization, cookie on its 200. A repeat that sends the tag back in If-None-Match gets a 304 with the same headers, no body and the securityFloor headers. It follows the same pattern as the forks route (#9172).

  • Access check first: the tag is computed only after the session check (401) and the readFlags permission check (403). Those two answers are unchanged and carry no ETag, even with If-None-Match: *.
  • Body unchanged: it is still JSON.stringify({ flags }), byte for byte. Because the tag is taken from those bytes, turning a flag on or off changes it, and two viewers shown different bodies can't share one. src/http/api.ts is untouched.

test/flags-etag.test.ts covers:

  • the 200's tag (it equals weakEtag of the body), the cache headers and the unchanged body;
  • the bodiless 304 with the floor headers;
  • the tag moving when the flag is set to { forge: true } and then { forge: false };
  • 401 and 403 with no ETag, both with * and when a non-admin replays an admin's tag.

Without the route change, the first three tests fail. The 401/403 test passes either way, because it pins down behaviour that already exists.

The earlier check failed only because no commit message had a Try: line. Force pushes are refused, so an empty follow-up commit adds it. Locally, the gate passes against origin/main, as do bun x tsc --noEmit and the flags tests (test/flags-etag.test.ts, test/api-flags.test.ts, test/flags-timing.test.ts). The issue asked for a plan kept with keep_plan, but that tool wasn't available in this session, so the issue has no plan.

Try: as a forge admin, run curl -i -H 'authorization: Bearer <token>' <preview>/api/v1/flags, then repeat it with -H 'if-none-match: <etag>' and expect a bodiless 304.

🤖 Generated with Claude Code

Approved

Clean, small change. The tag is computed only after the 401 and 403 checks, over the exact bytes sent. The 304 carries the same headers and no body, and the tests cover each case. I did not exercise the 200/304 on the preview, since it needs an admin app token; the preview answers 401 with the floor headers and no ETag.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs