Skip to content

fix(catalog): reject empty formulae/casks array from refresh endpoint#101

Merged
msitarzewski merged 1 commit into
msitarzewski:mainfrom
Kartalops:fix/arvuno/catalog-reject-empty-response
Jun 19, 2026
Merged

fix(catalog): reject empty formulae/casks array from refresh endpoint#101
msitarzewski merged 1 commit into
msitarzewski:mainfrom
Kartalops:fix/arvuno/catalog-reject-empty-response

Conversation

@Kartalops

Copy link
Copy Markdown
Contributor

After count_top_level_array succeeds the code previously continued unconditionally and persisted a fresh manifest with formula_count or cask_count equal to 0. formulae.brew.sh always returns thousands of entries on a healthy response, so a zero count is a strong signal that something went wrong upstream: a hostile mirror, a partial body, or a CDN error page that happens to parse as a JSON array.

Persisting such a response would silently zero out the user's catalog on the next launch. This change turns a zero count into a typed error so the caller (UI, auto-refresh helper) keeps the previous good catalog and surfaces a refresh failure to the user.

Non-empty behavior is unchanged; only an empty array short-circuits.

After count_top_level_array succeeds the code previously continued
unconditionally and persisted a fresh manifest with formula_count or
cask_count equal to 0. formulae.brew.sh always returns thousands of
entries on a healthy response, so a zero count is a strong signal that
something went wrong upstream: a hostile mirror, a partial body, or a
CDN error page that happens to parse as a JSON array.

Persisting such a response would silently zero out the user's catalog
on the next launch. This change turns a zero count into a typed error
so the caller (UI, auto-refresh helper) keeps the previous good catalog
and surfaces a refresh failure to the user.

Non-empty behavior is unchanged; only an empty array short-circuits.

@msitarzewski msitarzewski left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thank you for this, @Arvuno — this is exactly the kind of defensive thinking I want in the catalog path. 🙏

The reasoning in your description is spot on: a zero count after a successful parse really is a strong "something's wrong upstream" signal (hostile mirror / partial body / a CDN error page that happens to be a valid JSON array), and silently zeroing out the user's catalog on next launch would be a nasty failure mode.

I reviewed and verified it end to end:

  • Placement is correct — the guard short-circuits before gzip_compress / write_user_data and before the in-memory Arc swap, so a rejected refresh leaves the previous good catalog fully intact. Exactly as you described.
  • Callers degrade gracefully — the startup auto-refresh treats the error as non-fatal (keeps the stale catalog, logs a warning), and the manual Dashboard refresh surfaces the failure to the user. No panic path.
  • Linux is safe — my one concern was our Linux cask-gating, but cask.json is fetched unconditionally from the formulae.brew.sh API (always thousands of entries regardless of client OS); the Linux gating lives at a different layer, so this won't false-positive there.
  • Green locally — clippy clean (no new warnings) and all catalog tests pass.

Approving — this is ready to merge. One small follow-up I'll track on our side (not yours to fix): mirroring this same guard into the native Swift CatalogService to keep the two shells in data-contract parity.

Really appreciate the careful PR write-up — made the review easy. Hope to see more from you. 🍺

@msitarzewski msitarzewski merged commit a085547 into msitarzewski:main Jun 19, 2026
msitarzewski added a commit that referenced this pull request Jun 22, 2026
…sponse-guard

fix(catalog): reject empty refresh response in native shell (parity with #101)
pull Bot pushed a commit to cyberchen1995/brew-browser that referenced this pull request Jun 22, 2026
…rity with msitarzewski#101)

Native parity follow-up to Arvuno's Tauri fix (msitarzewski#101): when a catalog refresh
parses to a valid-but-empty JSON array, throw `CatalogRefreshError.emptyResponse`
BEFORE persisting or swapping the in-memory catalog, so a hostile mirror /
partial body / CDN error page can't silently zero out the user's catalog. The
guard sits before gzip/write, so a rejected refresh keeps the previous good
catalog — mirroring the Tauri `refresh_catalog_inner` guard exactly. Safe on
every OS: cask.json comes from the formulae.brew.sh API, not the OS-gated local
brew layer, so it's non-empty regardless of platform.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants