Skip to content

update.sh: revalidate API files with the ETag - #1

Open
xrl wants to merge 1 commit into
api-revalidate-stale-cachefrom
update-sh-revalidate-etag
Open

xrl wants to merge 1 commit into
api-revalidate-stale-cachefrom
update-sh-revalidate-etag

Conversation

@xrl

@xrl xrl commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Stacked on Homebrew#24193 (base is that PR's branch, api-revalidate-stale-cache). Opened on the fork only, because Homebrew allows one AI-assisted PR at a time; it will be retargeted to Homebrew/brew once Homebrew#24193 merges.


  • Have you followed our Contributing guidelines?
  • Have you checked for other open Pull Requests for the same change?
  • Have you explained what your changes do? Performance claims (e.g. "this is faster") must include brew benchmark results.
  • Have you explained why you'd like these changes included, not just what they do?
  • For bug fixes, have you given step-by-step brew commands to reproduce the bug?
  • Have you written new tests (excluding integration tests)? Here's an example.
  • Have you successfully run brew lgtm (style, typechecking and tests) locally?

  • I did not use AI/LLM to create this PR, or I disclosed the tool/model below and reviewed its output; I did not attribute commits to AI and will answer maintainer questions and review comments myself without AI/LLM.

Disclosure: drafted with Claude Code (Opus 5.5); diff and behaviour reviewed by me


Follow-up to Homebrew#24193, which fixes the same mtime/If-Modified-Since problem in the Ruby API path (Homebrew::API.fetch_json_api_file); this PR does the same for brew update's bash fetch.

fetch_api_file in cmd/update.sh downloads internal/packages.<tag>.jws.json with curl --time-cond <cache file> and then touches the file. If that body came from a stale CDN edge, its mtime is newer than the real object's Last-Modified, so every later brew update sends an If-Modified-Since the server answers with 304 and the stale index stays until the next publish. It also never writes the <file>.etag that Homebrew#24193 revalidates with, so after brew update writes a new body the Ruby path compares against an ETag for a different body.

This:

  • sends --etag-compare <file>.etag when an ETag is saved and curl is 7.68.0 or newer (--etag-save/--etag-compare need 7.68.0; Homebrew only requires 7.41.0), and falls back to --time-cond otherwise
  • saves the ETag to <file>.etag.new and only replaces <file>.etag when it is non-empty, because curl blanks the --etag-save file on a 304 and on an untagged 200; after a non-304 without an ETag (read from --write-out '%{http_code}') the old .etag is deleted
  • keeps the current OS's .etag when removing other OS versions' API files (brew cleanup --scrub keeps it in api: revalidate JSON API caches with ETag, not the bumped mtime聽Homebrew/brew#24193)

The --time-cond path, the exit 56 retry and the byte-size update detection are unchanged.

Repro (the mechanism, without needing a stale edge):

u=https://formulae.brew.sh/api/internal/packages.arm64_tahoe.jws.json
curl -sI "$u" | grep -i -e last-modified -e etag
# brew update's If-Modified-Since is the touched mtime, i.e. later than Last-Modified:
curl -s -o /dev/null -w '%{http_code}\n' -H "If-Modified-Since: $(date -u -v+1H '+%a, %d %b %Y %H:%M:%S GMT')" "$u"   # 304
# The same body revalidated by ETag answers 200 as soon as the content differs:
curl -s -o /dev/null -w '%{http_code}\n' -H 'If-None-Match: "not-the-current-etag"' "$u"   # 200

Checked against real curl 8.7.1 and a local server that sends an ETag, honours If-None-Match and keeps Last-Modified older than the local mtime (so --time-cond alone would 304):

run 1 (empty cache)   If-None-Match=None               -> 200  .etag="afbf9d0f3560b0fd"
run 2 (unchanged)     If-None-Match="afbf9d0f3560b0fd" -> 304  .etag kept
run 3 (body changed)  If-None-Match="afbf9d0f3560b0fd" -> 200  body updated, .etag="e37b2e5a95f958d3"
run 4 (unchanged)     If-None-Match="e37b2e5a95f958d3" -> 304

With --time-cond only (curl < 7.68.0), run 3 answers 304 and keeps the old body, which is the current behaviour.

馃 Generated with Claude Code

https://claude.ai/code/session_01XdswxjrNLFURYY68A6ZUSi

- `fetch_api_file` touches the cached `*.jws.json` to now after every
  check and revalidates with `curl --time-cond`. A body fetched from a
  stale CDN edge ends up with an mtime newer than the real object's
  `Last-Modified`, so the server answers `304` until the next publish.
- Save the ETag to `<file>.etag` and send `--etag-compare` when one is
  saved, as `Homebrew::API.fetch_json_api_file` does in Homebrew#24193. Fall
  back to `--time-cond` without a saved ETag or on curl older than
  7.68.0, which lacks `--etag-save` and `--etag-compare`.
- curl blanks the `--etag-save` file on a `304` and on an untagged
  `200`, so save to `<file>.etag.new`, replace `<file>.etag` only when
  it is non-empty and delete it after a non-`304` without an ETag.
  This keeps `.etag` in sync with whatever body `brew update` wrote.
- Keep the current OS's `.etag` when removing other OS versions' API
  files.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant