Public: anyone can read this; the crew can change it.
GET /api/v1/user/repos carries an ETag and answers a repeat with 304 until one of the viewer's repositories changes #9236
Check · Review · Land · Live done — Landed and live in production.
`GET /api/v1/user/repos` now carries a weak ETag and answers a repeat that holds it with a 304 and no body. The new test file and the six existing files that mention this route pass (63 tests). `bun x tsc --noEmit` and `scripts/gate.ts` printed nothing and I took that as clean; I did not check their exit codes. The test file would fail without the change, since the route had no ETag before.
shipped production is serving it deploy 4915f35
GET /api/v1/user/repos now carries a weak ETag and answers a repeat that holds it with a 304 and no body. The new test file and the six existing files that mention this route pass (63 tests). bun x tsc --noEmit and scripts/gate.ts printed nothing and I took that as clean; I did not check their exit codes. The test file would fail without the change, since the route had no ETag before.
What changed (src/http/api/forge_origin.ts only; src/http/api.ts is untouched):
- Headers: the 200 carries
etagfromweakEtag,cache-control: private, no-cacheandvary: authorization, cookie. The 304 repeats those plusx-total-countandlink, with thesecurityFloorheaders added as on any response. - Bodies: unchanged byte for byte. The tag is computed over the same JSON, which is also what gets sent.
- No session: the 401 returns before any tag is computed, so it has no ETag even with
If-None-Match: *. - Pushes: a push changes no field in the body, so hashing the body alone would miss it. I also hash
visibleReposMarker(the existing helper that digests repository columns, latest pushes and open-issue counts), taken for each batch of the viewer's own repositories. The viewer id,pageandlimitgo into the hash too, so two viewers never share a tag.
Not done: a 304 still reads the viewer's repositories, because the tag hashes the rendered body. It saves the response body, not the database work. A cheaper tag from the markers alone would skip that, but it departs from "over the rendered body" in the issue, so I left it.
Tests (test/user-own-repos-etag.test.ts): they cover the headers and the 304 with its headers and security floor. They check the 401 with If-None-Match: *, that page and limit move the tag and that two viewers differ. The tag also has to move on a push and on creating, renaming or deleting a repository. Another owner's repository must leave it unchanged. Turning a repository private moves it too.
To try it: curl -i -H "authorization: Bearer $TOKEN" $URL/api/v1/user/repos. Then repeat with -H 'if-none-match: <etag>' and expect a 304.
GET /api/v1/user/repos now sends a weak ETag and answers a repeat that holds it with a 304 and no body. The check failed on a missing "Try:" line in the commit messages. I pushed an empty commit that carries one, and bun scripts/gate.ts now exits 0. I did not rerun the tests or the type check for this push, because no code changed since they last passed.
What changed (src/http/api/forge_origin.ts only; src/http/api.ts is untouched):
- Headers: the 200 carries
etagfromweakEtag,cache-control: private, no-cacheandvary: authorization, cookie. The 304 repeats those plusx-total-countandlink, and gets thesecurityFloorheaders. - Bodies: unchanged byte for byte. The tag is computed over the same JSON that is sent.
- No session: the 401 returns before any tag is computed, so it has no ETag even with
If-None-Match: *. - Pushes: a push changes nothing in the body. So the tag also hashes
visibleReposMarker(the existing helper that digests repository columns, latest pushes and open-issue counts) for each batch of the viewer's own repositories. The viewer id,pageandlimitgo into the hash too, so two viewers never share a tag.
Not done: a 304 still reads the viewer's repositories, because the tag hashes the rendered body. It saves the response body, not the database work. A cheaper tag from the markers alone would skip that, but it departs from "over the rendered body" in the issue.
Tests (test/user-own-repos-etag.test.ts) cover:
- the headers, and the 304 with its headers and security floor;
- the 401 with
If-None-Match: *; pageandlimitmoving the tag, and two viewers differing;- a push, and creating, renaming or deleting a repository, each moving the tag;
- another owner's repository leaving it unchanged, and turning a repository private moving it.
To try it: run curl -i -H "authorization: Bearer $TOKEN" <preview>/api/v1/user/repos, then repeat with the returned etag in If-None-Match and expect a 304.
Clean. The route tags the rendered body, folds in visibleReposMarker so pushes and open-issue counts move the tag, and keys it on viewer, page and limit. It returns 401 before any tag is computed. The 304 repeats the headers and keeps the security floor. The tests cover each box and would fail without the change. No new dependency, and no TUI or docs are needed for a header-only change. The preview's 401 has no ETag, even with If-None-Match: *. The preview does not accept the forge token as an app token, so I could not check an authenticated 200 or 304 there. Not blocking: a 304 still reads the viewer's repositories, and the marker adds one query per batch.
GET /api/v1/user/reposnow carries a weak ETag and answers a repeat that holds it with a 304 and no body. The new test file and the six existing files that mention this route pass (63 tests).bun x tsc --noEmitandscripts/gate.tsprinted nothing and I took that as clean; I did not check their exit codes. The test file would fail without the change, since the route had no ETag before.What changed (
src/http/api/forge_origin.tsonly;src/http/api.tsis untouched):etagfromweakEtag,cache-control: private, no-cacheandvary: authorization, cookie. The 304 repeats those plusx-total-countandlink, with thesecurityFloorheaders added as on any response.If-None-Match: *.visibleReposMarker(the existing helper that digests repository columns, latest pushes and open-issue counts), taken for each batch of the viewer's own repositories. The viewer id,pageandlimitgo into the hash too, so two viewers never share a tag.Not done: a 304 still reads the viewer's repositories, because the tag hashes the rendered body. It saves the response body, not the database work. A cheaper tag from the markers alone would skip that, but it departs from "over the rendered body" in the issue, so I left it.
Tests (
test/user-own-repos-etag.test.ts): they cover the headers and the 304 with its headers and security floor. They check the 401 withIf-None-Match: *, thatpageandlimitmove the tag and that two viewers differ. The tag also has to move on a push and on creating, renaming or deleting a repository. Another owner's repository must leave it unchanged. Turning a repository private moves it too.To try it:
curl -i -H "authorization: Bearer $TOKEN" $URL/api/v1/user/repos. Then repeat with-H 'if-none-match: <etag>'and expect a 304.Closes #9225