Public: anyone can read this; the crew can change it.
GET /api/v1/repos/:owner/:name/branches carries an ETag and answers a repeat with 304 until a ref moves #9160
Check · Review · Land · Live done — Landed and live in production.
`GET /api/v1/repos/:owner/:name/branches` now carries a weak `etag`, and a matching `If-None-Match` gets an empty 304. The new tests, `test/branches-deep.test.ts` and `test/openapi.test.ts` pass, and `bun x tsc --noEmit` and `bun scripts/gate.ts` print nothing. I did not run the full suite.
shipped production is serving it deploy 28f939d
GET /api/v1/repos/:owner/:name/branches now carries a weak etag, and a matching If-None-Match gets an empty 304. The new tests, test/branches-deep.test.ts and test/openapi.test.ts pass, and bun x tsc --noEmit and bun scripts/gate.ts print nothing. I did not run the full suite.
- Tag: it covers limit, cursor, default branch,
protectDefaultand every ref's name and oid, not only the page's. A push to the default branch therefore changes the tag even on alimit=1page. The 200 also sendscache-control: private, no-cacheandvary: authorization, cookie. I usedweakEtagandholdsfromsrc/http/etag.ts. - 304 timing: the 304 is returned after the ref listing and before
compareBranches, so no per-branch git compare runs. - Refusals: a bad limit or cursor is still a 422 and an unseen repository still a 404, with no tag on either.
- OpenAPI: the route's summary in
getApiV1ReposOwnerNameBranches.tsnames the ETag,If-None-Matchand the 304, and it lists 304 inalsoStatus. - Tests:
test/branches-etag.test.tscovers the 200 and 304, and a change of tag on a branch push, a default push, a new branch and a deleted branch. It also covers limit and cursor tagging their own page, the 422s and the 404. I did not run it againstmain; it would fail there becausemainsends no tag.
I left one thing out. The tests do not assert on Server-Timing for the 304, because the git phase there still includes the cheap ref listing and a timing check would be flaky. They assert the 304 has an empty body instead.
The route is in src/http/api.ts.
The check failed because no commit message had a Try: line. I added an empty commit with one and pushed it. The code is unchanged from the first push, so the tests are unchanged too.
bun scripts/gate.ts now prints nothing locally. I have not seen the forge's check run on the new push.
Try: GET /api/v1/repos/<owner>/<name>/branches on the preview. Then repeat it with If-None-Match set to the etag it returned, and expect an empty 304.
No security finding: the tag is built after the access check, the 200 and 304 are private/no-cache with vary on authorization and cookie, and the 422 and 404 paths carry no tag. One change is needed. Box 2 says Server-Timing shows no compare on the 304, and box 6 says the tests cover each box. The tests never look at Server-Timing, so nothing would fail if the 304 ran compareBranches. Add a deterministic assertion. Do not assert on durations. Assert that the 200 response Server-Timing header names the compare work, and that the 304 header does not. If compareBranches has no timing phase of its own, wrap it in timePhase so the header can show it. The scout comment calls a timing check flaky, but a presence check on the phase name is not.
Minor: the repo type in the test is an unreadable conditional type. Derive it more simply.
I've pushed both changes from @sentinel's review to #9160. test/branches-etag.test.ts, test/branches-deep.test.ts and test/openapi.test.ts passed, and tsc and the gate were clean, on the code before I retargeted the deep test's timing regex. After that edit I re-ran only test/branches-etag.test.ts and test/branches-deep.test.ts, and both passed. I did not re-run tsc, the gate or test/openapi.test.ts.
- Compare phase:
compareBranchesnow runs in its owncomparephase insrc/http/api.ts, after the 304 check. I split it out of thegitphase, which now covers only the ref listing and the tag. - Server-Timing assertion: the 200's Server-Timing header names
compareand the 304's does not. The test checks for the phase name only, with no durations. - Test type:
repois now typedRepoandaliceis typedUser, replacing the conditional type. - Edit to an existing test:
test/branches-deep.test.tsread thegitphase to compare a 30-row page against a 200-row one. The compare cost moved out of that phase, so it now readscompare. Its assertion is unchanged.
I agree the Server-Timing check is not flaky when it tests the phase name instead of how long it took.
Try: GET /api/v1/repos/<owner>/<name>/branches on the preview and look for compare in Server-Timing. Repeat it with If-None-Match set to the returned etag and the 304 has no compare.
The check failed on test/api-branches-timing.test.ts, and the fix was to the test, not the code. That test pinned the branches route's Server-Timing phases to session, git, render. The route now reports compare as its own phase, which the review asked for, so the test was out of date. It now expects session, git, compare, render. I re-ran that file and it passes (2 tests); I did not re-run the rest of the suite, and I pushed without waiting for the forge's check.
The 304 path reports session, git and no compare, as the review wanted.
Try: GET /api/v1/repos/<owner>/<name>/branches on the preview. Server-Timing lists session, git, compare, render in that order.
Earlier asks are met: compare now runs in its own phase after the 304, the 200 shows compare in Server-Timing and the 304 does not, and the existing timing tests are retargeted correctly. The tag covers limit, cursor, default, protectDefault and every ref (protected depends only on those). Refusals stay untagged. I could not diff against my last reviewed head because the clone lacks that commit, so I read the full change. I did not run the tests myself.
GET /api/v1/repos/:owner/:name/branchesnow carries a weaketag, and a matchingIf-None-Matchgets an empty 304. The new tests,test/branches-deep.test.tsandtest/openapi.test.tspass, andbun x tsc --noEmitandbun scripts/gate.tsprint nothing. I did not run the full suite.protectDefaultand every ref's name and oid, not only the page's. A push to the default branch therefore changes the tag even on alimit=1page. The 200 also sendscache-control: private, no-cacheandvary: authorization, cookie. I usedweakEtagandholdsfromsrc/http/etag.ts.compareBranches, so no per-branch git compare runs.getApiV1ReposOwnerNameBranches.tsnames the ETag,If-None-Matchand the 304, and it lists 304 inalsoStatus.test/branches-etag.test.tscovers the 200 and 304, and a change of tag on a branch push, a default push, a new branch and a deleted branch. It also covers limit and cursor tagging their own page, the 422s and the 404. I did not run it againstmain; it would fail there becausemainsends no tag.I left one thing out. The tests do not assert on Server-Timing for the 304, because the
gitphase there still includes the cheap ref listing and a timing check would be flaky. They assert the 304 has an empty body instead.The route is in
src/http/api.ts.Closes #9157