Skip to content

download_strategy: fix caching of :latest downloads#20137

Merged
MikeMcQuaid merged 1 commit into
masterfrom
cached-location-validation
Jun 18, 2025
Merged

download_strategy: fix caching of :latest downloads#20137
MikeMcQuaid merged 1 commit into
masterfrom
cached-location-validation

Conversation

@EricFromCanada

@EricFromCanada EricFromCanada commented Jun 17, 2025

Copy link
Copy Markdown
Member

First, the cached_location_valid = false if v.is_a?(Cask::DSL::Version) && v.latest? line was never being run, because version is one of (NilClass, String, Version), not Cask::DSL::Version. It's intended to ensure that :latest casks are always re-downloaded on each upgrade, but with what we have now we can do better than that.

Further down is the cached location invalidation logic, which compares the cached file's modification date and file size against the results from a HEAD request on the download URL and invalidates if either are different. An earlier PR made this logic never run if the final download URL was a redirect. For anything with the version number in the URL this isn't usually a problem, but for resources behind a public URL that stays static and redirects to a new secondary URL on each update, they end up never being updated, e.g. :latest casks hosted on GitHub or GCloud or AWS.

Currently the above change doesn't appear to be necessary (although this may not have always been the case). For example, this is the redirect that occurs for downloading chromium:

$ curl -IL https://download-chromium.appspot.com/dl/Mac_Arm?type=snapshots
HTTP/2 302 
content-type: text/html; charset=utf-8
location: https://commondatastorage.googleapis.com/chromium-browser-snapshots/Mac_Arm/1475052/chrome-mac.zip
x-cloud-trace-context: 18003b12623c5b9f91c3034bb31cedc6
content-length: 383
date: Tue, 17 Jun 2025 17:49:24 GMT
server: Google Frontend
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

HTTP/2 200 
content-type: application/zip
x-guploader-uploadid: ABgVH88K3wiPIBKfU8U_vEO8Wqc-gPDIbdIK7aQtq4HTcXqpXjUSMZI-QTTbSPJQEP3FrCSC
expires: Tue, 17 Jun 2025 18:49:24 GMT
date: Tue, 17 Jun 2025 17:49:24 GMT
cache-control: public, max-age=3600
last-modified: Tue, 17 Jun 2025 17:35:45 GMT
etag: "956331c642e10e3be369a3a1c97a3eeb"
x-goog-generation: 1750181745929583
x-goog-metageneration: 1
x-goog-stored-content-encoding: identity
x-goog-stored-content-length: 147858426
x-goog-hash: crc32c=GqJrPQ==
x-goog-hash: md5=lWMxxkLhDjvjaaOhyXo+6w==
x-goog-storage-class: STANDARD
accept-ranges: bytes
content-length: 147858426
server: UploadServer
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

Note how the first request has a small content-length value. With some odebug added to download_strategy.rb, we see that this isn't considered when evaluating the upstream file size.

$ brew fetch --cask -d chromium
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromNameLoader): loading chromium
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromPathLoader): loading /opt/homebrew/Library/Taps/homebrew/homebrew-cask/Casks/c/chromium.rb
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 -V
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 --fail --location --silent --head https://download-chromium.appspot.com/dl/Mac_Arm\?type=snapshots
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 --fail --location --silent --head --request GET --http1.1 https://download-chromium.appspot.com/dl/Mac_Arm\?type=snapshots
==> Downloading https://download-chromium.appspot.com/dl/Mac_Arm?type=snapshots
==>   upstream file_size: 147858426
==> Downloading from https://commondatastorage.googleapis.com/chromium-browser-snapshots/Mac_Arm/1475052/chrome-mac.zip
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --fail --progress-bar --retry 3 --remote-time --output /Users/eric/Library/Caches/Homebrew/downloads/9689e554ae2d938027c2d26f29176cdf362d24ff5f9f0bd338550e4a524e40f5--chrome-mac.zip.incomplete --location https://commondatastorage.googleapis.com/chromium-browser-snapshots/Mac_Arm/1475052/chrome-mac.zip
################################################################################################# 100.0%
...
Downloaded to: /Users/eric/Library/Caches/Homebrew/downloads/9689e554ae2d938027c2d26f29176cdf362d24ff5f9f0bd338550e4a524e40f5--chrome-mac.zip
SHA256: 2ebf334f3a8f8999e7d93c72f8afc7ba928f3e70d279bdcdeff07d852a29b134

$ brew fetch --cask -d chromium
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromNameLoader): loading chromium
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromPathLoader): loading /opt/homebrew/Library/Taps/homebrew/homebrew-cask/Casks/c/chromium.rb
==> Downloading https://download-chromium.appspot.com/dl/Mac_Arm?type=snapshots
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 -V
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 --fail --location --silent --head https://download-chromium.appspot.com/dl/Mac_Arm\?type=snapshots
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 --fail --location --silent --head --request GET --http1.1 https://download-chromium.appspot.com/dl/Mac_Arm\?type=snapshots
==>   upstream file_size: 147858426
==> cached_location.size: 147858426
Already downloaded: /Users/eric/Library/Caches/Homebrew/downloads/9689e554ae2d938027c2d26f29176cdf362d24ff5f9f0bd338550e4a524e40f5--chrome-mac.zip
...
SHA256: 2ebf334f3a8f8999e7d93c72f8afc7ba928f3e70d279bdcdeff07d852a29b134

With an older nightly build of wezterm@nightly cached, we currently get:

$ brew fetch --cask -d wezterm@nightly 
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromNameLoader): loading wezterm@nightly
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromPathLoader): loading /opt/homebrew/Library/Taps/homebrew/homebrew-cask/Casks/w/wezterm@nightly.rb
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 -V
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 --fail --location --silent --head https://github.com/wezterm/wezterm/releases/download/nightly/WezTerm-macos-nightly.zip
==> Downloading https://github.com/wezterm/wezterm/releases/download/nightly/WezTerm-macos-nightly.zip
==>   upstream file_size: 112521600
==> cached_location.size: 112521859
Already downloaded: /Users/eric/Library/Caches/Homebrew/downloads/c57ec1344ecbf5ea80285241b68da2b5ba5475b74af75d8da93f1090e9badafa--WezTerm-macos-nightly.zip
...
SHA256: bbe95c7b8a95c525f4eb5c0d2e40df1b9f4e6802b1b388357c6e823896b2a0e3

After making these changes:

$ brew fetch --cask -d wezterm@nightly 
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromNameLoader): loading wezterm@nightly
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromPathLoader): loading /opt/homebrew/Library/Taps/homebrew/homebrew-cask/Casks/w/wezterm@nightly.rb
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 -V
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 --fail --location --silent --head https://github.com/wezterm/wezterm/releases/download/nightly/WezTerm-macos-nightly.zip
==> Downloading https://github.com/wezterm/wezterm/releases/download/nightly/WezTerm-macos-nightly.zip
==>   upstream file_size: 112521600
==> cached_location.size: 112521859
==> Downloading from https://objects.githubusercontent.com/github-production-release-asset-2e65be/120568143/4ce0fc9b-b2a4-4207-a007-96a0d50bb67c?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=releaseassetproduc
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --fail --progress-bar --retry 3 --remote-time --output /Users/eric/Library/Caches/Homebrew/downloads/c57ec1344ecbf5ea80285241b68da2b5ba5475b74af75d8da93f1090e9badafa--WezTerm-macos-nightly.zip.incomplete --location https://objects.githubusercontent.com/github-production-release-asset-2e65be/120568143/4ce0fc9b-b2a4-4207-a007-96a0d50bb67c\?X-Amz-Algorithm=AWS4-HMAC-SHA256\&X-Amz-Credential=releaseassetproduction\%2F20250617\%2Fus-east-1\%2Fs3\%2Faws4_request\&X-Amz-Date=20250617T180837Z\&X-Amz-Expires=300\&X-Amz-Signature=803c8697ae843fef0310c92614e852fa79b5493eaace6b57b89689ba396c69f1\&X-Amz-SignedHeaders=host\&response-content-disposition=attachment\%3B\%20filename\%3DWezTerm-macos-nightly.zip\&response-content-type=application\%2Foctet-stream
################################################################################################# 100.0%
...
SHA256: b9f48ca00383f887579f26ccb64121ec8452f391a408f83e47d3824e69abef0f

$ brew fetch --cask -d wezterm@nightly 
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromNameLoader): loading wezterm@nightly
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromPathLoader): loading /opt/homebrew/Library/Taps/homebrew/homebrew-cask/Casks/w/wezterm@nightly.rb
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 -V
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 --fail --location --silent --head https://github.com/wezterm/wezterm/releases/download/nightly/WezTerm-macos-nightly.zip
==> Downloading https://github.com/wezterm/wezterm/releases/download/nightly/WezTerm-macos-nightly.zip
==>   upstream file_size: 112521600
==> cached_location.size: 112521600
Already downloaded: /Users/eric/Library/Caches/Homebrew/downloads/c57ec1344ecbf5ea80285241b68da2b5ba5475b74af75d8da93f1090e9badafa--WezTerm-macos-nightly.zip
...
SHA256: b9f48ca00383f887579f26ccb64121ec8452f391a408f83e47d3824e69abef0f

Fixes #20084. If there's still cases of cached locations being invalidated when they shouldn't be, please comment below. @boblail

@EricFromCanada
EricFromCanada force-pushed the cached-location-validation branch from 305167e to 5ff58d2 Compare June 17, 2025 18:51

@MikeMcQuaid MikeMcQuaid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again @EricFromCanada!

@MikeMcQuaid
MikeMcQuaid added this pull request to the merge queue Jun 18, 2025
Merged via the queue into master with commit 76470c0 Jun 18, 2025
31 checks passed
@MikeMcQuaid
MikeMcQuaid deleted the cached-location-validation branch June 18, 2025 07:47
@vvvvv

vvvvv commented Jun 19, 2025

Copy link
Copy Markdown

Appreciate the quick fix @EricFromCanada .

@boblail

boblail commented Jun 24, 2025

Copy link
Copy Markdown
Contributor

@EricFromCanada, this is actually a breaking change for my scenario.

We have a proxy (Cloudflare) in front of a private host for internal bottles. If the user's authentication has lapsed, the proxy will redirect to a login page that serves HTML whose Content-Length has nothing to do with the cached downloads.

To increase the determinism of our devenv, we seed Homebrew's downloads cache bottles we know engineers will need. We wouldn't want a redirect to Cloudflare's login page to invalidate this cache.

Notably, the login page does not serve a Content-Disposition header 🤔 We seem to expect that of the HEAD request but not the GET request. Would it make sense to disregard redirects that don't end in a response that includes content-disposition: attachment...?

I've made a change to our private tap that bypasses this logic, so I'm not blocked; but I wanted to describe a use-case where the cache invalidation logic might not quite be dialed in yet.

@EricFromCanada

Copy link
Copy Markdown
Member Author

Thanks for the background info. Seems logical; I'll attempt to cook up a test case.

@EricFromCanada

Copy link
Copy Markdown
Member Author

@boblail What's the (anonymized) output of curl -IL for the Cloudflare login page? It seems the presence of Content-Disposition isn't as reliable an indicator that an HTTP response is downloadable as the presence of Content-Type starting with "text" is that it's not downloadable.

@boblail

boblail commented Jun 30, 2025

Copy link
Copy Markdown
Contributor

@boblail What's the (anonymized) output of curl -IL for the Cloudflare login page? It seems the presence of Content-Disposition isn't as reliable an indicator that an HTTP response is downloadable as the presence of Content-Type starting with "text" is that it's not downloadable.

@EricFromCanada,

Here's what I ran

$ curl -IL https://{HOST}/{BOTTLE_PATH}

HTTP/2 302
date: Mon, 30 Jun 2025 18:24:13 GMT
content-type: text/html
content-length: 143
location: https://{TENANT}.cloudflareaccess.com/cdn-cgi/access/login/{HOST}?{ARGS}
set-cookie: {COOKIE}
access-control-allow-credentials: true
cache-control: private, max-age=0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0
expires: Thu, 01 Jan 1970 00:00:01 GMT
set-cookie: {COOKIE}
server: cloudflare
cf-ray: 957fb3065beab9f8-SEA

HTTP/2 200
date: Mon, 30 Jun 2025 18:24:14 GMT
content-type: text/html
content-length: 30070
set-cookie: {COOKIE}
strict-transport-security: max-age=31536000; includeSubDomains
cf-access-domain: {HOST}
cf-version: 20-f198bbf
content-security-policy: frame-ancestors 'none'; connect-src 'self' http://127.0.0.1:*; default-src https: 'unsafe-inline'
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
server: cloudflare
cf-ray: 957fb306e969fa3b-SEA

@EricFromCanada

Copy link
Copy Markdown
Member Author

@boblail See if #20200 is a fix for your situation.

@boblail

boblail commented Jul 13, 2025

Copy link
Copy Markdown
Contributor

@EricFromCanada, yep! That fix works for my scenario 👍 Thanks!

I can remove the monkey patch I referenced earlier:

I've made a change to our private tap that bypasses this logic, so I'm not blocked...

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.

Casks with :latest and :no-check do not update

4 participants