brew update can stick on a stale internal/packages.*.jws.json after a 304 + touch (outdated shows old cask version) #6997
Summary
Filing here for discussion/confirmation before deciding whether it warrants a Environment (
|
| Source | cursor version |
|---|---|
~/Library/Caches/Homebrew/api/internal/packages.arm64_tahoe.jws.json (local) |
3.14.27,… |
https://formulae.brew.sh/api/internal/packages.arm64_tahoe.jws.json (live) |
3.15.6,… |
api/cask/cursor.json (local and live) |
3.15.6,… (already fresh) |
brew updateprinted Already up-to-date and advanced the localpackages.*.jws.jsonmtime, but did not replace the body (local size15597989vs live15609041;cmpdiffered).- Because installed-cask
outdated/inforead the internal packages index (not per-caskapi/cask/*.json), brew believed3.14.27was current.--greedy-auto-updatesdidn't help — brew saw no newer version at all. - Fix:
rm -f ~/Library/Caches/Homebrew/api/internal/packages.* brew update brew outdated --cask cursor # → cursor (now detected)
Suspected mechanism
Library/Homebrew/cmd/update.sh → fetch_api_file() does a conditional fetch then unconditionally touches the cache on any curl success (including 304):
api_curl_download "${json_url}" "${cache_path}" "${time_cond[@]}" # --time-cond <cache_path>
...
if [[ ${curl_exit_code} -eq 0 ]]; then
touch "${cache_path}" # runs even on HTTP 304 Not Modified
...
fiIf the local body is ever behind origin (e.g. a CDN edge validates/serves an older representation while origin already published a newer Last-Modified), a 304 + touch advances the local mtime. Once local mtime is newer than the current object's Last-Modified, every later conditional request keeps returning 304 and the on-disk body stays stale — so the index never refreshes without a manual delete / brew cleanup.
Reproduced the header behavior against the live CDN while in the bad state:
# The poisoned cache mtime, as curl actually sends it (verified with -v):
# If-Modified-Since: Mon, 10 Aug 2026 03:21:04 GMT
# The object I was stuck missing had:
# Last-Modified: Mon, 10 Aug 2026 03:01:44 GMT
# 03:21:04 > 03:01:44, so every conditional request returns 304 and the stale body persists.
# older If-Modified-Since → 200 + the new body
curl -sSI -H "If-Modified-Since: Tue, 05 Aug 2026 00:00:00 GMT" \
https://formulae.brew.sh/api/internal/packages.arm64_tahoe.jws.json
# HTTP/2 200 ; last-modified: Mon, 10 Aug 2026 03:01:44 GMT ; content-length: 15609041Correction to an earlier draft of this post: I originally quoted the cache mtime as 10:21:04 GMT; that was local time (UTC+7) mislabelled as GMT. The true value is 03:21:04 GMT, which is what curl --time-cond <file> sends. The conclusion is unchanged (and the margin is 19m20s, not 7h).
Why touch exists (the underlying tension)
The same mtime is used for two incompatible purposes:
| Consumer | Wants mtime to mean |
|---|---|
skip_download? in api.rb ((Time.now - stale_seconds) < target.mtime) |
"last validated at" → now |
--time-cond "${cache_path}" in api_curl_download |
"Last-Modified of the body we hold" → the object's timestamp |
api_curl_download passes --remote-time, so after a 200 the mtime is the object's Last-Modified — and then touch overwrites it with now to satisfy the TTL check. That is what lets the validator drift ahead of the content it is supposed to describe.
The Ruby path (Homebrew::API.fetch_json_api_file) has the same "freshen mtime after conditional success" shape — it sets download_succeeded = true and FileUtils.touch after a curl that includes 304. This is related to but distinct from #22089 / #22093, which fixed touching mtime after a failed download.
Questions for maintainers
- Is the
touchafter a 304 intentional? On 304 the body is unchanged, so advancing mtime past the object'sLast-Modifiedseems to be what poisons future conditional requests. - Would setting the cache mtime from
--remote-time/Last-Modified(and not unconditionallytouch-ing on 304) be the right fix, so a temporarily stale CDN response can't stick permanently? - Should there be a guard when
api/cask/<token>.jsonis newer than the internal packages index (surface a mismatch / force refresh)?
Not deterministic (CDN/timing dependent) and already recovered on my machine, hence Discussion rather than a Bug issue. Happy to open a PR against fetch_api_file if maintainers agree on the direction.
Replies: 1 comment
|
The issue here is your system clock is wrong. You should fix this. It being wrong will affect a wide variety of software. |
The issue here is your system clock is wrong. You should fix this. It being wrong will affect a wide variety of software.