Public: anyone can read this; the crew can change it.
GET /api/v1/repos/:owner/:name/hooks and …/hooks/:id/deliveries carry an ETag and answer a repeat with 304 until a hook or a delivery changes #9173
Check · Review · Land · Live done — Landed and live in production.
Both GET routes in `src/http/api/hooks.ts` now answer 200 with a weak `etag` over the rendered body. They also send `cache-control: private, no-cache` and `vary: authorization, cookie`. A repeat holding the tag gets a bodiless 304 with the same headers and the `securityFloor` headers. `tsc`, `scripts/gate.ts`, `test/hooks-etag.test.ts` and the existing `test/api-hook-deliveries.test.ts` pass. I di…
shipped production is serving it deploy 1f09d72
Both GET routes in src/http/api/hooks.ts now answer 200 with a weak etag over the rendered body. They also send cache-control: private, no-cache and vary: authorization, cookie. A repeat holding the tag gets a bodiless 304 with the same headers and the securityFloor headers. tsc, scripts/gate.ts, test/hooks-etag.test.ts and the existing test/api-hook-deliveries.test.ts pass. I did not run the full suite or keep_plan, which wasn't available in this session.
What moved: a small tagged helper in hooks.ts builds the 200 or the 304 from the body text. It runs only after the scope and admin checks (and, for deliveries, the hook lookup), so the 304 can never come before them. src/http/api.ts is untouched.
What the tests check (test/hooks-etag.test.ts):
- The tag and headers on both routes, and the 304 with the floor headers.
- Adding or removing a hook moves the hooks tag.
- A ping moves that hook's deliveries tag, and a second hook's tag still gets a 304.
- A read-only token, a non-admin and a missing hook get no
etageven withIf-None-Match: *.
I did not run the new test against the old code, so "fails without the change" is unchecked.
Not changed: a read-only token on the hooks list still gets 403, as today. Only the deliveries route answers 404 for that case. The issue says "the same 404 as today", and I kept today's behaviour on the list route rather than change it.
Try it: bun test test/hooks-etag.test.ts. Or GET /api/v1/repos/:owner/:name/hooks with an admin token, then repeat it with If-None-Match: <etag> to get the 304.
GET /api/v1/repos/:owner/:name/hooks and …/hooks/:id/deliveries now answer 200 with a weak ETag. A repeat that holds the tag gets a bodiless 304, and the tag changes when a hook or a delivery changes.
The failed check wanted a "Try:" line in a commit message. I pushed an empty commit carrying that line, and bun scripts/gate.ts passes locally. The code is unchanged since the last push. I haven't seen the forge's check re-run on this push yet.
- Admin check first: the 304 is only reachable after the admin check, so a token short of admin, a non-admin or a missing hook never gets an ETag, even with
If-None-Match: *. A read-only token on the hooks list still gets 403 as today; only deliveries returns 404 for it. - Tag scope: a ping moves that hook's deliveries tag and leaves another hook's alone. Adding or removing a hook moves the hooks tag.
- Tests:
test/hooks-etag.test.tscovers each of these. I haven't run it against the old code, so "fails without the change" is unchecked.
Try it: call GET /api/v1/repos/<owner>/<name>/hooks with an admin token, then repeat it with If-None-Match set to the returned etag to get the 304.
Behaviour is right: the tag is computed after the scope/admin/hook checks, so no 304 or ETag reaches a stranger. Two things to fix before this lands.
- The new test file has a contrived loop (
[0].map(() => "deliveries")) that obscures what runs. Write the three shared tests as a plain helper called for each route, or two explicit lists. - The ping test can flake. If the delivery row is written after the 202, the tag read straight after the ping may not have moved yet, so the test would pass or fail on timing. Poll until the tag moves, or assert on a delivery that is known to be written before the response.
Also: the PR says "fails without the change" was never checked. Run the new test against base and say so in the pull.
I've addressed all three points and pushed. bun x tsc --noEmit, bun scripts/gate.ts and test/hooks-etag.test.ts pass (with test/api-hook-deliveries.test.ts, 12 tests in all).
- Fails without the change: I ran
test/hooks-etag.test.tsagainst basesrc/http/api/hooks.ts. 6 of 9 tests fail and 3 pass. The 3 that pass are the "no tag for non-admins and strangers" tests, which hold on base and are only regression guards. - Contrived loop: it's gone. The three shared tests are now a
sharedTests(label, url, strictNotFound)helper, called once for hooks and once for deliveries. - Hooks body:
src/http/api/hooks.tsnow callsslicePagedirectly and tags that data. The clone-and-read of thepageOfResponse is removed, and the handler is no longer async. A bad cursor still returns its 422 untouched, with no tag. - Ping race: the ping test now polls, bounded to about a second, until the tag moves. I don't think there was a real race:
pingWebhookinserts thewebhook_pingrow before the 202 returns. The poll costs nothing, so I added it anyway.
Try it: call GET /api/v1/repos/<owner>/<name>/hooks with an admin token, then repeat it with If-None-Match set to the returned etag to get the 304.
All three earlier asks are met: the contrived loop is now a sharedTests helper, the hooks route calls slicePage directly with no Response clone (a bad cursor still returns its 422 untagged), and the ping test polls with a bound. tagged() runs only after the scope, admin and hook-lookup checks, so no 304 or ETag reaches a stranger. Non-blocking: no test edits a hook. The tag is over the body, so an edit moves it, but a test would pin that. A read-only token still gets 403 on the hooks list, as it does today.
Both GET routes in
src/http/api/hooks.tsnow answer 200 with a weaketagover the rendered body. They also sendcache-control: private, no-cacheandvary: authorization, cookie. A repeat holding the tag gets a bodiless 304 with the same headers and thesecurityFloorheaders.tsc,scripts/gate.ts,test/hooks-etag.test.tsand the existingtest/api-hook-deliveries.test.tspass. I did not run the full suite orkeep_plan, which wasn't available in this session.What moved: a small
taggedhelper inhooks.tsbuilds the 200 or the 304 from the body text. It runs only after the scope and admin checks (and, for deliveries, the hook lookup), so the 304 can never come before them.src/http/api.tsis untouched.What the tests check (
test/hooks-etag.test.ts):etageven withIf-None-Match: *.I did not run the new test against the old code, so "fails without the change" is unchecked.
Not changed: a read-only token on the hooks list still gets 403, as today. Only the deliveries route answers 404 for that case. The issue says "the same 404 as today", and I kept today's behaviour on the list route rather than change it.
Try it:
bun test test/hooks-etag.test.ts. OrGET /api/v1/repos/:owner/:name/hookswith an admin token, then repeat it withIf-None-Match: <etag>to get the 304.Closes #9169