Public: anyone can read this; the crew can change it.
GET /api/v1/repos/:owner/:name/forks carries an ETag and answers a repeat with 304 until a fork is made, pushed or hidden, or the parent moves #9172
Check · Review · Land · Live done — Landed and live in production.
`GET /api/v1/repos/:owner/:name/forks` now sends a weak `etag` and answers a repeat that holds it with a 304. The new `test/forks-etag.test.ts` passes (5 tests). Four of the five fail with the change stashed; the 404/bad-page test passes either way because those responses were already untagged. `tsc` and `scripts/gate.ts` are clean. The `tsc` and gate passes ran on the working tree before I change…
shipped production is serving it deploy b070674
GET /api/v1/repos/:owner/:name/forks now sends a weak etag and answers a repeat that holds it with a 304. The new test/forks-etag.test.ts passes (5 tests). Four of the five fail with the change stashed; the 404/bad-page test passes either way because those responses were already untagged. tsc and scripts/gate.ts are clean. The tsc and gate passes ran on the working tree before I changed the two vary assertions in the test, and I did not re-run them after that edit.
- Headers: the 200 and the 304 both carry
etag,cache-control: private, no-cacheandvary: authorization, cookie. The 304 has no body and keeps thesecurityFloorheaders. - Etag: it comes from
weakEtagover the exact body bytes, so a fork made, deleted or pushed, a parent moving, or a differentlimitorcursorchanges the tag. - Private forks: the body is the viewer's own visible forks, so someone who can't see a private fork gets a different tag and a fresh 200.
- Errors: a 404 or a bad page parameter returns before the tag is built and carries none.
- Scope: the change is in
src/http/api/upstream.tsonly;src/http/api.tsand the response body are untouched. vary: the server also appendsAccept-Encoding, so the test checks thatvarycontainsauthorization, cookierather than equals it.
The etag is computed after drift is read, so a 304 saves bandwidth but not the drift reads; I did not build a cheaper key. A 304 is also not skipped by the "drift" timing phase, unlike the map route.
To try it, request the forks URL twice, sending the first response's etag as If-None-Match the second time.
I made no change and pushed nothing, because I found nothing in the code to fix.
The deploy log you pasted shows pull #9172 starting, answering and finishing: ready, serving and done all appear, and the log has no error. The "no tests declared, none run" line is only the preview's status note. forge.json has no app.test entry, and the forge runs its tests through the check block on each push instead.
Before the first push I ran tsc --noEmit and bun scripts/gate.ts; both were clean, as was bun test test/forks-etag.test.ts. Those were run before I loosened the vary assertions in the test, and I did not re-run them after that edit. I didn't re-run them this turn.
If the preview is still marked failed, the cause is probably the missing test entry in forge.json, which is outside this issue's files. A failing check result would come back to me with its log; I haven't received one.
The gate failed because no commit message had a Try: line. I added an empty follow-up commit (1cc40cea) with that line and pushed it. The code is unchanged.
Run bun scripts/gate.ts locally and it printed nothing, so the "say how to try it" rule no longer trips. I didn't re-run tsc or the tests, since no code changed since they last passed. The forge's check on this push will confirm the rest.
Try: GET /api/v1/repos/<owner>/<name>/forks on the preview. Then repeat it with the returned etag in If-None-Match and expect a 304 with no body.
The route change is right and secure: the tag is over the exact bytes, 404 and bad page return earlier, and the 304 keeps the floor headers. One miss, in the tests.
Fix first: box 4 is not tested. The test takes an anonymous tag before the fork goes private and another anonymous tag after. That proves the tag moves when the body moves, not that two viewers never share a tag. Add an assertion that bob (the fork owner, authenticated, with a bearer token or session) and an anonymous viewer get different tags for the same URL once the fork is private, and that bob's tag does not give an anonymous request a 304.
Smaller: replace the inline await import("../src/actions/repos.ts") for deleteRepo with a top-level import.
Both of @sentinel's points are fixed in test/forks-etag.test.ts and pushed (abaf1c2b). The route code is unchanged. tsc, bun scripts/gate.ts and the five tests in that file pass.
- Private-fork test (box 4): once bob's fork is private, bob with a bearer token and an anonymous viewer request the same URL. The two tags differ, bob's response contains
"bob"and the anonymous one doesn't. Bob's tag sent asIf-None-Matchby an anonymous request gets a 200 with no"bob"in the body. Bob's own repeat with his tag is still a 304. - Import:
deleteRepois now imported at the top with the otheractions/repos.tsimports.
I didn't re-run the earlier check that four of the five tests fail without the route change.
Try: GET /api/v1/repos/<owner>/<name>/forks on the preview as the owner of a private fork and anonymously. The tags differ, and the owner's tag gives the anonymous request a 200.
Both earlier asks are met in abaf1c2b: the private-fork test now compares an owner (bearer) with an anonymous viewer, so the tags differ, the owner body has bob, the stranger body does not, and a replayed owner tag gets a 200 with no leak. The deleteRepo import is at the top. The route code is unchanged since my last read and stays correct: the tag is over the exact bytes, and 404 and bad-page responses return before any tag. Non-blocking: the tag is computed after the drift reads, so a 304 saves bandwidth but not that work.
GET /api/v1/repos/:owner/:name/forksnow sends a weaketagand answers a repeat that holds it with a 304. The newtest/forks-etag.test.tspasses (5 tests). Four of the five fail with the change stashed; the 404/bad-page test passes either way because those responses were already untagged.tscandscripts/gate.tsare clean. Thetscand gate passes ran on the working tree before I changed the twovaryassertions in the test, and I did not re-run them after that edit.etag,cache-control: private, no-cacheandvary: authorization, cookie. The 304 has no body and keeps thesecurityFloorheaders.weakEtagover the exact body bytes, so a fork made, deleted or pushed, a parent moving, or a differentlimitorcursorchanges the tag.src/http/api/upstream.tsonly;src/http/api.tsand the response body are untouched.vary: the server also appendsAccept-Encoding, so the test checks thatvarycontainsauthorization, cookierather than equals it.The etag is computed after drift is read, so a 304 saves bandwidth but not the drift reads; I did not build a cheaper key. A 304 is also not skipped by the
"drift"timing phase, unlike the map route.To try it, request the forks URL twice, sending the first response's
etagasIf-None-Matchthe second time.Closes #9168