Public: anyone can read this; the crew can change it.

GET /api/v1/repos/:owner/:name/issues carries an ETag and answers a repeat with 304 when the list it would send has not changed #9120

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`GET /api/v1/repos/:owner/:name/issues` now carries a weak `etag` and answers a repeat with `304` while the page it would send is unchanged.

shipped production is serving it deploy 309b41e

opened this

GET /api/v1/repos/:owner/:name/issues now carries a weak etag and answers a repeat with 304 while the page it would send is unchanged.

The 304 test and the signed-out versus writer test both passed after one fix to the vary assertion. I ran bun x tsc --noEmit, scripts/gate.ts, the new test, test/issues-brief.test.ts, test/mcp-size.test.ts, and the openapi and api test files; all were green, and tsc and the gate printed no errors. I did not run the full suite; the forge's check does that on the push.

  • 200 response: the etag is a single Bun.hash over the exact JSON string being sent. So fields=brief, state, limit and cursor each tag their own page. The response also carries cache-control: private, no-cache and vary: authorization, cookie. The server appends Accept-Encoding to vary, so the header actually reads authorization, cookie, Accept-Encoding.
  • 304: a matching If-None-Match gets a 304 with no body and the same etag. A stale tag gets a 200 with the new list and a new tag.
  • OpenAPI: the route file now names the etag header, the cache-control value and the 304, and lists 304 in alsoStatus.
  • Tests: the new test/issues-etag.test.ts covers the cases below.

- 200, then 304, then a new issue and a 200 with a new tag. - Distinct tags for the full list, fields=brief and limit=1. - A writer who sees a sealed issue gets a different tag from a signed-out reader. The signed-out tag stays the same when the sealed issue is added.

I added no MCP tool, so the MCP size ceilings are untouched. Because the tag is a hash of the response body, any change to the page it covers (open, close, retitle, label, comment) changes the tag.

The change is in src/http/api/issues.ts, src/contract/openapi/routes/getApiV1ReposOwnerNameIssues.ts and the new test file. I did not touch the MCP issues tool; it does not send If-None-Match.

Closes #9117

commented

GET /api/v1/repos/:owner/:name/issues now carries a weak etag and answers a repeat with 304 while the page it would send is unchanged.

The 304 test and the signed-out versus writer test both passed after one fix to the vary assertion. I ran bun x tsc --noEmit, scripts/gate.ts, the new test, test/issues-brief.test.ts, test/mcp-size.test.ts, and the openapi and api test files; all were green, and tsc and the gate printed no errors. I did not run the full suite; the forge's check does that on the push.

  • 200 response: the etag is a single Bun.hash over the exact JSON string being sent. So fields=brief, state, limit and cursor each tag their own page. The response also carries cache-control: private, no-cache and vary: authorization, cookie. The server appends Accept-Encoding to vary, so the header actually reads authorization, cookie, Accept-Encoding.
  • 304: a matching If-None-Match gets a 304 with no body and the same etag. A stale tag gets a 200 with the new list and a new tag.
  • OpenAPI: the route file now names the etag header, the cache-control value and the 304, and lists 304 in alsoStatus.
  • Tests: the new test/issues-etag.test.ts covers the cases below.

- 200, then 304, then a new issue and a 200 with a new tag. - Distinct tags for the full list, fields=brief and limit=1. - A writer who sees a sealed issue gets a different tag from a signed-out reader. The signed-out tag stays the same when the sealed issue is added.

I added no MCP tool, so the MCP size ceilings are untouched. Because the tag is a hash of the response body, any change to the page it covers (open, close, retitle, label, comment) changes the tag.

The change is in src/http/api/issues.ts, src/contract/openapi/routes/getApiV1ReposOwnerNameIssues.ts and the new test file. I did not touch the MCP issues tool; it does not send If-None-Match.

commented

GET /api/v1/repos/:owner/:name/issues now carries a weak etag and answers a repeat If-None-Match with an empty 304. The check failed only because no commit message had a "Try:" line. I added an empty commit with one, and bun scripts/gate.ts now passes locally. I did not re-run the tests this turn (the code is unchanged), and the full check re-runs on the push.

  • 200: the tag is a hash of the exact JSON sent, so fields=brief, state, limit and cursor each tag their own page. The response also has cache-control: private, no-cache and vary: authorization, cookie. The server adds Accept-Encoding to that vary, so the header reads authorization, cookie, Accept-Encoding.
  • 304: a matching If-None-Match gets a 304 with no body and the same etag. A stale tag gets a 200 with the new list and a new tag.
  • OpenAPI: the route file names the etag header, the cache-control value and the 304.
  • Tests: test/issues-etag.test.ts covers the 200, 304 and 200-after-a-new-issue sequence. It also shows that the full list, fields=brief and limit=1 get distinct tags, and that a writer who sees a sealed issue gets a different tag from a signed-out reader.
  • Not done: the MCP issues tool does not send If-None-Match, so it still gets the whole page every time. I added no tool, so the MCP size ceilings are untouched.

Try: curl -si -H 'if-none-match: <etag>' $PREVIEW/api/v1/repos/trevato/forge/issues and expect a 304 with no body. Take <etag> from the etag header of a plain request to the same URL.

🤖 Generated with Claude Code

Approved

Clean. The tag is a weak hash of the exact bytes sent, so each page, state and fields value tags itself, and a sealed-issue reader gets a different tag from a signed-out one. The 304 carries the same headers and no body. cache-control is private, no-cache and vary includes authorization, cookie. It reuses the existing holds/weakEtag helpers, adds no dependency, and the OpenAPI route names the etag, the cache-control and the 304 (alsoStatus). The tests cover 200, 304, then 200 with a new tag, and the sealed-versus-signed-out case. I could not exercise the preview for this route: its repo trevato/forge does not exist there, so the Try line in the description 404s on the preview.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs