Skip to content

utils/curl: request headers when body is unused - #23528

Merged
MikeMcQuaid merged 1 commit into
mainfrom
curl-head-https
Aug 15, 2026
Merged

utils/curl: request headers when body is unused#23528
MikeMcQuaid merged 1 commit into
mainfrom
curl-head-https

Conversation

@krehel

@krehel krehel commented Aug 14, 2026

Copy link
Copy Markdown
Member

Today, curl_check_http_content verifies that a URL is reachable, but does so with a full GET: it passes --output and downloads the entire resource, then discards it. For an HTTPS URL the body is never read, so this asks for the headers instead.


  • 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 Hyperfine benchmarks.
  • 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.

Opus 5 with several manual review cycles.


I was investigating Cask audit and seeing where I could speed them up, and started exploring.

brew audit --online currently transfers every cask artifact twice: once by curl_check_http_content into a Tempfile that is unlinked on return, and once by audit_download into the cache. For Firefox (as an example) that is 147 MB downloaded and thrown away on every audit. Because online implies download, both always happen in the same run.

Across a sampling of Casks, speedup was anywhere from 1.5x to 45x faster, network speeds will affect individual results.

Formula were a bit different, realizing slight speedups but much less notable, and on a few smaller downloads took a small amount longer, but that could be network jitter with how small the variances are.


Details

Hyperfine statistics

Casks — hyperfine --runs 3

Benchmark 1: before (full GET)
  Time (mean ± σ):     187.485 s ± 15.742 s   Range: 171.562 s … 203.041 s   3 runs
Benchmark 2: after (HEAD)
  Time (mean ± σ):      14.552 s ±  1.099 s   Range:  13.316 s …  15.420 s   3 runs

Summary: after ran 12.88 ± 1.46 times faster
arm mean σ median min max individual runs
before (full GET) 187.485 s 15.742 s 187.852 s 171.562 s 203.041 s 187.9 / 203.0 / 171.6
after (HEAD) 14.552 s 1.099 s 14.919 s 13.316 s 15.420 s 14.9 / 13.3 / 15.4

Roughly 172.9 s saved per pass.

Formulae — hyperfine --runs 5

Benchmark 1: before (full GET)
  Time (mean ± σ):     142.368 s ±  5.629 s   Range: 138.099 s … 151.768 s   5 runs
Benchmark 2: after (HEAD)
  Time (mean ± σ):      60.892 s ± 10.368 s   Range:  52.917 s …  77.816 s   5 runs

Summary: after ran 2.34 ± 0.41 times faster
arm mean σ median min max individual runs
before (full GET) 142.368 s 5.629 s 139.867 s 138.099 s 151.768 s 151.8 / 143.3 / 138.8 / 139.9 / 138.1
after (HEAD) 60.892 s 10.368 s 56.472 s 52.917 s 77.816 s 53.6 / 56.5 / 63.6 / 77.8 / 52.9

Roughly 81.5 s saved per pass.


Times may vary based upon network

Individual sampling results

Casks

cask before (ms) after (ms) speedup saved (ms)
insomnia 15,811 358 44.1× 15,453
inkscape 6,267 145 43.2× 6,122
discord 7,976 186 42.9× 7,790
obs 25,063 612 40.9× 24,451
postman 5,901 153 38.6× 5,748
zoom 7,893 248 31.9× 7,645
tableplus 4,927 230 21.4× 4,697
visual-studio-code 12,172 579 21.0× 11,593
vlc 25,091 1,284 19.5× 23,807
slack 4,991 308 16.2× 4,683
firefox 6,155 400 15.4× 5,755
spotify 6,539 432 15.1× 6,107
google-chrome 11,011 768 14.3× 10,243
iterm2 1,836 132 13.9× 1,704
gimp 9,196 863 10.7× 8,333
cyberduck 7,317 782 9.4× 6,534
keka 1,909 334 5.7× 1,574
sequel-ace 1,702 316 5.4× 1,386
audacity 1,612 360 4.5× 1,252
maccy 946 273 3.5× 673
handbrake-app 3,245 1,084 3.0× 2,161
stats 892 312 2.9× 580
transmission 960 353 2.7× 607
rectangle 711 390 1.8× 320
appcleaner 759 477 1.6× 282
TOTAL (25) 170,882 11,379 15.0× 159,503

Formulae

Formulae

formula before (ms) after (ms) speedup saved (ms)
cmake 21,413 2,039 10.5× 19,374
go 16,760 2,947 5.7× 13,812
python@3.13 4,094 1,196 3.4× 2,898
git 24,057 7,343 3.3× 16,714
node 4,653 1,866 2.5× 2,787
postgresql@16 2,499 1,146 2.2× 1,353
imagemagick 2,623 1,563 1.7× 1,060
bat 3,090 1,955 1.6× 1,135
neovim 7,493 4,642 1.6× 2,851
redis 1,712 1,063 1.6× 649
pandoc 3,261 2,092 1.6× 1,169
protobuf 1,775 1,217 1.5× 558
jq 2,589 1,812 1.4× 777
nginx 1,228 883 1.4× 345
fd 2,626 2,088 1.3× 538
libgit2 2,386 1,780 1.3× 606
openssl@3 912 720 1.3× 192
htop 1,885 1,580 1.2× 305
sqlite 827 677 1.2× 150
zstd 1,328 1,140 1.2× 188
tmux 1,753 1,525 1.1× 228
curl 1,499 1,483 1.0× 16
ffmpeg 19,981 20,569 0.97× −588
ripgrep 2,279 2,432 0.94× −153
TOTAL (24) 132,723 65,758 2.02× 66,965

Copilot AI lite review requested due to automatic review settings August 14, 2026 18:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR optimizes curl_check_http_content to avoid downloading full response bodies when they are not needed (notably for HTTPS reachability checks), reducing unnecessary network transfer during audits while preserving existing comparison behavior for HTTP→HTTPS checks.

Changes:

  • Switch HTTPS-only reachability checks to request headers (HEAD) instead of downloading bodies.
  • Add a fallback to retry with GET when a server rejects HEAD.
  • Add unit tests covering the new head_only behavior and retry logic.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
Library/Homebrew/utils/curl.rb Adds head_only support to avoid body downloads on HTTPS checks and introduces retry behavior.
Library/Homebrew/test/utils/curl_spec.rb Adds coverage for head_only curl args and HEADGET retry behavior.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread Library/Homebrew/utils/curl.rb Outdated
Comment thread Library/Homebrew/utils/curl.rb

@p-linnane p-linnane left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks, nice win. The HEAD→GET fallback on any failure status is the right call (servers reject HEAD with all sorts of codes, 403 from WAFs especially, and retrying broadly means we still report the GET's status like before).

One change I'd like: a server that drops HEAD without ever sending a status line (curl exit 52/56/28) never gets the GET fallback. It just re-HEADs through the retry loop and reports "not reachable". The "failure was below HTTP" comment in the test doesn't quite hold there; the server has already seen the method by the time it hangs up. Could you extend the fallback to also flip to GET when there's no status code and the exit status is 28/52/56? DNS/connect failures (6/7) can stay as-is since those really are method-independent.

On the Copilot comments: I'd skip both. Narrowing the retry to 405/501 would turn HEAD-hostile 403s into false audit failures, and the unused Tempfile on the HEAD path is negligible next to the network round trip.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (4)

Library/Homebrew/utils/curl.rb:609

  • curl_http_content_headers_and_checksum always creates a Tempfile, even when head_only is true and the body is never written/read. This adds avoidable filesystem work on the new fast-path. Consider only allocating the Tempfile when !head_only and making the ensure cleanup conditional (e.g. file&.unlink).
      file = Tempfile.new.tap(&:close)

Library/Homebrew/utils/curl.rb:496

  • head_rejected currently treats any non-1xx/2xx status_code from the HEAD response as “HEAD rejected”, which causes an unconditional retry as a GET (and potentially a full download) for genuine failures like 404/500. This adds an extra request without improving the outcome. Restrict the HEADGET fallback to status codes that indicate the method is not supported (e.g. 405/501), keeping the existing exit-status-based fallback for cases where no HTTP status is available.
          head_rejected = if (status_code = details[:status_code])
            !http_status_ok?(status_code)
          else
            CURL_REQUEST_SENT_EXIT_CODES.include?(details[:exit_status])
          end

Library/Homebrew/utils/curl.rb:491

  • This comment is incomplete (trailing comma) and doesn’t clearly explain the else branch; it reads like it was meant to continue. Please reword so it’s a complete sentence.
          # Some servers reject `HEAD` but serve `GET`.
          # DNS and connection failures happen before the request,

Library/Homebrew/test/utils/curl_spec.rb:732

  • The new HEADGET fallback behavior is only tested for 405. Add coverage for a “normal” HTTP failure (e.g. 404) to ensure we don’t do an unnecessary GET retry when HEAD returns a valid non-2xx response.
    it "retries as a `GET` when the server rejects `HEAD`" do
      recorded = record_head_only(response.merge(status_code: "405"), response)
      curl_check_http_content("https://brew.sh/", "homepage URL")
      expect(recorded).to eq([true, false])
    end

@krehel
krehel requested a review from p-linnane August 14, 2026 20:21

@p-linnane p-linnane left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks!

Comment thread Library/Homebrew/utils/curl.rb Outdated
Comment thread Library/Homebrew/utils/curl.rb Outdated

@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!

@MikeMcQuaid
MikeMcQuaid added this pull request to the merge queue Aug 15, 2026
Merged via the queue into main with commit c68efb6 Aug 15, 2026
45 checks passed
@MikeMcQuaid
MikeMcQuaid deleted the curl-head-https branch August 15, 2026 07:41
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.

4 participants