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

GET /api/v1/cloud/accounts carries an ETag and answers a repeat with 304 until a cloud account or its use changes #9235

Merged opened this

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

Merged

`GET /api/v1/cloud/accounts` now carries a weak ETag and answers a repeat with a 304. The new test file and the existing `test/api-cloud-accounts.test.ts` pass, and `tsc` and `scripts/gate.ts` are clean. I did not run the full suite; the forge runs it on the push. With the `src` change stashed, all four new tests fail.

shipped production is serving it deploy d5155e2

opened this

GET /api/v1/cloud/accounts now carries a weak ETag and answers a repeat with a 304. The new test file and the existing test/api-cloud-accounts.test.ts pass, and tsc and scripts/gate.ts are clean. I did not run the full suite; the forge runs it on the push. With the src change stashed, all four new tests fail.

  • Change: the only source edit is in src/http/api/cloud.ts. The tag is weakEtag of the exact JSON bytes sent, so the body is unchanged byte-for-byte. A 200 carries etag, cache-control: private, no-cache and vary: authorization, cookie. A request whose If-None-Match holds the tag gets a 304 with the same headers and no body, and securityFloor still adds its headers. I did not touch src/http/api.ts.
  • Access checks: the tag logic runs only after the checks for no session (401), a read-only token (403) and a non-admin (404). Those answer as before with no ETag, even with If-None-Match: *.
  • Test: test/cloud-accounts-etag.test.ts covers the headers and the 304, the tag moving when an account is linked or removed or a repository starts or stops a vendor, and the three refusals above.
  • Two viewers: I could not build two admins who see different bodies, because a forge admin sees every repository. The test instead asserts that the tag always equals the hash of the bytes sent, so viewers shown different bodies cannot share one.
  • One-minute lag: a repository's cloud.json is cached for up to a minute by usesByVendor, which was already so. A start or stop of use therefore moves the tag when that cache refreshes, not instantly. The test advances Date.now past the minute to prove the tag moves.
  • Left out: the page and MCP cloud_accounts are untouched.

Closes #9206

commented

GET /api/v1/cloud/accounts now carries a weak ETag and answers a repeat with a 304. The new test file and the existing test/api-cloud-accounts.test.ts pass, and tsc and scripts/gate.ts are clean. I did not run the full suite; the forge runs it on the push. With the src change stashed, all four new tests fail.

  • Change: the only source edit is in src/http/api/cloud.ts. The tag is weakEtag of the exact JSON bytes sent, so the body is unchanged byte-for-byte. A 200 carries etag, cache-control: private, no-cache and vary: authorization, cookie. A request whose If-None-Match holds the tag gets a 304 with the same headers and no body, and securityFloor still adds its headers. I did not touch src/http/api.ts.
  • Access checks: the tag logic runs only after the checks for no session (401), a read-only token (403) and a non-admin (404). Those answer as before with no ETag, even with If-None-Match: *.
  • Test: test/cloud-accounts-etag.test.ts covers the headers and the 304, the tag moving when an account is linked or removed or a repository starts or stops a vendor, and the three refusals above.
  • Two viewers: I could not build two admins who see different bodies, because a forge admin sees every repository. The test instead asserts that the tag always equals the hash of the bytes sent, so viewers shown different bodies cannot share one.
  • One-minute lag: a repository's cloud.json is cached for up to a minute by usesByVendor, which was already so. A start or stop of use therefore moves the tag when that cache refreshes, not instantly. The test advances Date.now past the minute to prove the tag moves.
  • Left out: the page and MCP cloud_accounts are untouched.
commented

GET /api/v1/cloud/accounts now carries a weak ETag and answers a repeat with a 304. The failed check was only the missing "Try:" line, so I added an empty commit carrying it and pushed. scripts/gate.ts passes locally. I didn't rerun the tests, since the commit is empty.

What changed

  • In src/http/api/cloud.ts, a 200 now has etag (from weakEtag over the exact bytes sent), cache-control: private, no-cache and vary: authorization, cookie. The body is unchanged byte-for-byte.
  • A repeat holding that tag in If-None-Match gets a 304 with the same headers, no body and the security-floor headers.
  • The tag logic runs only after the access checks. No session (401), a read-only token (403) and a non-admin (404) answer as before with no ETag, even with If-None-Match: *.
  • test/cloud-accounts-etag.test.ts covers the headers and the 304, the tag moving when an account is linked or removed or a repository starts or stops a vendor, and the three refusals. It fails without the change.

What I chose not to do

  • I did not touch src/http/api.ts, the page, or MCP cloud_accounts.
  • I could not build two admins who see different bodies, because an admin sees every repository. The test instead asserts that the tag always equals the hash of the bytes sent, so different bodies cannot share a tag.
  • A repository's cloud.json is cached for up to a minute (existing behaviour). A repository starting or stopping a vendor therefore moves the tag when that cache refreshes, not instantly.

Try: as a forge admin, GET /api/v1/cloud/accounts on the preview with an rw bearer token. Repeat it with the returned etag in If-None-Match and you get a bodiless 304.

🤖 Generated with Claude Code

Approved

Clean and matches the sibling ETag pulls. The tag is weakEtag over the exact bytes sent, so viewers shown different bodies cannot share one. The 304 is reached only after the 401/403/404 checks. Headers and content-type match json(). The tests cover each behaviour. No README change, no new dependency. The preview answers 401 unauthenticated, as it should.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs