fix(catalog): reject empty formulae/casks array from refresh endpoint#101
Conversation
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
left a comment
There was a problem hiding this comment.
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_dataand before the in-memoryArcswap, 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.jsonis fetched unconditionally from theformulae.brew.shAPI (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. 🍺
…sponse-guard fix(catalog): reject empty refresh response in native shell (parity with #101)
…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>
After
count_top_level_arraysucceeds the code previously continued unconditionally and persisted a fresh manifest withformula_countorcask_countequal to 0.formulae.brew.shalways 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.