Public: anyone can read this; the crew can change it.
GET /api/v1/repos/:owner/:name/app and …/app/env carry an ETag and answer a repeat with 304 until the app or its environment changes #9232
Check · Review · Land · Live done — Landed and live in production.
`GET /api/v1/repos/:owner/:name/app` and `…/app/env` now carry a weak ETag, and a repeat holding it gets a bodiless 304. The new tests pass, along with the existing `test/app-reads-etag.test.ts`. `tsc` is clean and the gate script printed nothing. With the `src/http/api/app.ts` change stashed, 5 of the 6 new tests fail; the one that passes anyway is the refusals test, which only checks behaviour t…
shipped production is serving it deploy aafe9dc
GET /api/v1/repos/:owner/:name/app and …/app/env now carry a weak ETag, and a repeat holding it gets a bodiless 304. The new tests pass, along with the existing test/app-reads-etag.test.ts. tsc is clean and the gate script printed nothing. With the src/http/api/app.ts change stashed, 5 of the 6 new tests fail; the one that passes anyway is the refusals test, which only checks behaviour that was already there.
What changed
- Both routes in
src/http/api/app.tsnow answer through the module's existingtaggedhelper. - The 200 carries
etag,cache-control: private, no-cacheandvary: authorization, cookie. The 304 repeats those headers and keeps the security-floor headers. - Bodies are unchanged. The tag is a hash of the rendered body, so it moves when the environments list changes (start, stop, new commit) or a variable name is added or removed. Variable values are never in the body, so they never reach the tag.
- Two viewers who are shown different bodies get different tags, because each tag is computed from the body that viewer sees.
- Nothing changed on the refusal paths, and
src/http/api.tsis untouched. The 304 is only built once the access check has passed, so scope and access refusals and a missing repository answer as before with no ETag, even withIf-None-Match: *.
Test
test/app-state-etag.test.tscovers each of these points.- Start, stop and deploy are simulated by stubbing
runtime.statusrather than running a real app, so the test proves the environments feed the tag but not a real start or stop.
I did not run the whole suite; the forge's check does that on push.
🤖 Generated with Claude Code
The check failed because no commit message had a Try: line. I added an empty commit with one and pushed it. The gate now prints nothing locally. I did not re-run the tests, since only the commit message changed. The forge will run the check again on this push.
The Try: line says to GET /api/v1/repos/<owner>/<name>/app on the preview, then repeat the request with If-None-Match set to the returned etag to get a 304.
Nothing else changed since my earlier summary: GET …/app and …/app/env carry a weak ETag and answer a repeat with a bodiless 304. The new tests are in test/app-state-etag.test.ts.
🤖 Generated with Claude Code
Clean, minimal: both GETs go through the existing tagged helper after the access checks, so refusals and missing repos are untouched and never carry an ETag. The tag hashes the per-viewer body, so viewers shown different bodies never share one, and env values are never in the body. Tests cover each point. Caveat: the preview has no app on trevato/forge, so I could not show a 200/304 there; the tests carry it. Start/stop is simulated by stubbing runtime.status.
GET /api/v1/repos/:owner/:name/appand…/app/envnow carry a weak ETag, and a repeat holding it gets a bodiless 304. The new tests pass, along with the existingtest/app-reads-etag.test.ts.tscis clean and the gate script printed nothing. With thesrc/http/api/app.tschange stashed, 5 of the 6 new tests fail; the one that passes anyway is the refusals test, which only checks behaviour that was already there.What changed
src/http/api/app.tsnow answer through the module's existingtaggedhelper.etag,cache-control: private, no-cacheandvary: authorization, cookie. The 304 repeats those headers and keeps the security-floor headers.src/http/api.tsis untouched. The 304 is only built once the access check has passed, so scope and access refusals and a missing repository answer as before with no ETag, even withIf-None-Match: *.Test
test/app-state-etag.test.tscovers each of these points.runtime.statusrather than running a real app, so the test proves the environments feed the tag but not a real start or stop.I did not run the whole suite; the forge's check does that on push.
🤖 Generated with Claude Code
Closes #9227