Skip to content

fix(cli): re-download the browser when the cached archive is corrupt [P0] - #1982

Merged
miguel-heygen merged 1 commit into
mainfrom
fix/browser-corrupt-archive-redownload
Jul 7, 2026
Merged

fix(cli): re-download the browser when the cached archive is corrupt [P0]#1982
miguel-heygen merged 1 commit into
mainfrom
fix/browser-corrupt-archive-redownload

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

Problem

A partially-downloaded or interrupted chrome-headless-shell archive left in the cache makes @puppeteer/browsers' install() throw invalid end-of-central-directory during extraction (a zip whose central directory is truncated/missing). That error propagated straight out of downloadBrowser → the browser check, hard-blocking the render with no recovery. Users had to manually clear the cache to render again; several fell back to the deterministic local-frame renderer instead. This is a recurring Windows failure.

Fix

In packages/cli/src/browser/manager.ts, detect the corrupt-archive extraction error and self-heal:

  • isCorruptArchiveError(err) — pure predicate matching truncated/corrupt-zip signatures (end of central directory, not a zip, invalid or corrupt, unexpected end of, corrupted), while not matching network errors (ECONNRESET, etc.).
  • installWithCorruptArchiveRecovery(runInstall, clearCache, onRecover) — runs the install; on a corrupt-archive error it clears the cache (dropping the bad archive) and retries the download exactly once. Non-corruption errors propagate unchanged, and a second corruption propagates too (no infinite retry).
  • downloadBrowser wires the two together, clearing CACHE_DIR and warning before the single retry.

Network errors and genuine platform problems are unaffected — only a corrupt cached archive triggers the clear-and-redownload.

Verification

packages/cli/src/browser/manager.test.ts — 6 new assertions (21 total pass): predicate matches corrupt signatures / rejects network errors; recovery clears cache + retries once then succeeds; a non-corruption error propagates without clearing; a second corruption propagates (no infinite loop).

A partially-downloaded or interrupted chrome-headless-shell archive left in the
cache makes @puppeteer/browsers' install() throw "invalid
end-of-central-directory" during extraction. That error propagated out of the
browser check and hard-blocked the render, forcing users onto the fallback
renderer until they manually cleared the cache — a recurring Windows failure.

Detect the corrupt-archive extraction error (isCorruptArchiveError), clear the
cache to drop the bad archive, and retry the download exactly once; non-corrupt
errors and a second corruption still propagate (no infinite retry). The pure
predicate and the recovery wrapper are unit-tested.
@miguel-heygen miguel-heygen changed the title fix(cli): re-download the browser when the cached archive is corrupt [P0] fix(cli): re-download the browser when the cached archive is corrupt Jul 7, 2026
@miguel-heygen
miguel-heygen marked this pull request as ready for review July 7, 2026 19:04

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

R1 — #1982 at 83e89063e4a26ac9c97d386581db532bb4adfa66

🟢 LGTM. Clean self-heal, bounded retry, real unit coverage. One required-check blocker + two small nits.

Correctness

  • isCorruptArchiveError matches the six known truncated/corrupt-zip signatures with a lowercase substring check and explicitly rejects network errors (ECONNRESET, ENOENT, socket hang up). Predicate lives at pure-fn-level — the test that pins the boundary is doing real work.
  • installWithCorruptArchiveRecovery is a single-retry wrapper: on corrupt → onRecoverclearCache → one retry; on non-corrupt → propagate unchanged; on second corrupt → propagate (no infinite loop). All three branches unit-tested. Order of onRecover (before clearCache) is fine — the warn is future-tense.
  • CACHE_DIR = ~/.cache/hyperframes/chrome (verified at head) is our dedicated subdirectory, so rmSync(recursive: true, force: true) here doesn't clobber ~/.cache/puppeteer/chrome-headless-shell or any user data outside our cache.
  • normalizeErrorMessage correctly imported from ../utils/errorMessage.js; embedded in the warn with original case, lowercased inside the predicate. Consistent.
  • Only fires when install() throws during extraction. A silent-truncation failure (install succeeds but binary is broken) isn't in scope here — that's a different failure mode and belongs elsewhere.

Blocker

  • Semantic PR title CI check FAILED on the current head (SUCCESS on the older SHA at 06:32, FAILURE at 19:02 after the title got [P0] prefixed). Same as #2025. Drop the [P0] from the title or move it to a label. Required-check-shaped — block merge until it's green.

Nits (non-blocking)

  • msg.includes("corrupted") is broad — one-word substring, could false-positive on unrelated errors that happen to say "corrupted." The tradeoff is right (miss-a-variant hard-blocks the render; false-positive triggers a rare re-download), but consider tightening to "file is corrupted" / "archive is corrupted" if you want to be defensive.
  • Cache scope on recovery is the whole CACHE_DIR, not the failing archive subdir. Fine given the cache is dedicated and single-tenant, but a more surgical rm on just the chrome-headless-shell/<version>/<platform> folder would preserve any manually-placed browsers a user has staged. Micro-nit — the current scope matches how clearBrowser() already treats this dir.

R1 by Via

@miguel-heygen miguel-heygen changed the title [P0] fix(cli): re-download the browser when the cached archive is corrupt fix(cli): re-download the browser when the cached archive is corrupt [P0] Jul 7, 2026
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

@via thanks. Title fixed the same way (priority tag moved to the end). One extra note: I dug into the 'already-stuck box' case (archive gone, empty install dir) and it's already handled outside this predicate — the resolve path returns a staleInstallPath and callers run purgeStaleInstall + re-download, covered by the existing 'points at a missing binary' test. So no change needed there; this PR's corrupt-archive scope stays as reviewed.

@miguel-heygen
miguel-heygen merged commit 92f3116 into main Jul 7, 2026
86 of 87 checks passed
@miguel-heygen
miguel-heygen deleted the fix/browser-corrupt-archive-redownload branch July 7, 2026 21:10
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