Skip to content

fix: cross-platform Chrome detection with sane fallbacks (Linux/macOS) - #2

Open
arcahyadi wants to merge 2 commits into
Sterbweise:mainfrom
arcahyadi:fix/linux-chrome-detection
Open

fix: cross-platform Chrome detection with sane fallbacks (Linux/macOS)#2
arcahyadi wants to merge 2 commits into
Sterbweise:mainfrom
arcahyadi:fix/linux-chrome-detection

Conversation

@arcahyadi

Copy link
Copy Markdown

Summary

findChromePath() in app/lib/pdfGenerator.ts only handled Windows. On Linux and macOS it returned undefined, so PDF generation depended entirely on Puppeteer's own cache download. When that cache is missing or corrupt, rendering fails hard on non-Windows systems.

The problem in detail

1. Hardcoded developer-machine path

The very first candidate was a path baked into the source for one specific machine:

"C:\\Users\\Killian\\.cache\\puppeteer\\chrome\\win64-144.0.7559.96\\chrome-win64\\chrome.exe",

This does nothing on other machines (wrong username, wrong pinned version 144.0.7559.96 vs the one the current Puppeteer actually pins), and process.env["USERPROFILE"] variant still only runs on Windows.

2. Linux/macOS got no candidates at all

findChromePath() ends with return undefined; for every non-Windows platform. The caller then falls back to puppeteer.launch(launchOptions) with no executablePath, which makes Puppeteer resolve its cache directory (~/.cache/puppeteer). If a pinned browser is not there, launch fails:

Failed to generate PDF: Could not find Chrome (ver. 145.0.7632.67). This can occur if either
  1. you did not perform an installation before running the script
     (e.g. `npx puppeteer browsers install chrome`) or
  2. your cache path is incorrectly configured (which is: /home/<user>/.cache/puppeteer).

3. Real-world failure mode observed

On an Ubuntu 24.04 (LXC) machine, the pinned Chrome download had been interrupted mid-extraction: the cache contained ~/.cache/puppeteer/chrome/linux-145.0.7632.67/chrome-linux64/ with support folders (ABOUT, MEIPreload, WidevineCdm, ...) but no chrome binary (~131 MB of ~175 MB). Two aggravating effects:

  • Every PDF render failed with Could not find Chrome (ver. 145.0.7632.67), even though /usr/bin/google-chrome was installed and perfectly usable.

  • Re-running npx puppeteer browsers install chrome refused to repair it, because the version folder already exists:

    - DefaultProvider: The browser folder (/home/<user>/.cache/puppeteer/chrome/linux-145.0.7632.67) exists
      but the executable (.../chrome-linux64/chrome) is missing
    

    The user must manually rm -rf the corrupt folder before the installer will retry — a trap that is invisible from the error message.

So the app had a single point of failure (the cache), no system-browser fallback outside Windows, and no diagnostic output about what it looked for.

What this PR changes

findChromePath() is now cross-platform and ordered by priority:

  1. PUPPETEER_EXECUTABLE_PATH — the standard Puppeteer env override, checked first so deployments can pin any browser explicitly.
  2. puppeteer.executablePath() — Puppeteer's own pinned cache binary, wrapped in try/catch. Previously an inconsistent/corrupt cache made the whole call throw; now it's just a non-candidate and we fall through.
  3. Well-known system locations per OS:
    • Linux: /usr/bin/google-chrome, /usr/bin/google-chrome-stable, /usr/bin/chromium, /usr/bin/chromium-browser, /usr/bin/microsoft-edge, /snap/bin/chromium, /opt/google/chrome/chrome
    • macOS: /Applications/Google Chrome.app/..., /Applications/Chromium.app/..., /Applications/Microsoft Edge.app/...
    • Windows: unchanged list (system Chrome + Edge), minus the hardcoded C:\Users\Killian\... entry.

The search loop now also logs every candidate path it probes (not only on Windows), so the next "Could not find Chrome" incident comes with diagnostics instead of a dead end.

Testing

On Ubuntu 24.04 (LXC) against next dev:

  • tsc --noEmit — clean.
  • Normal case (pinned cache binary present): POST /api/convert-pdfHTTP 200, valid 1-page PDF; process list confirms the pinned chrome-linux64/chrome is used.
  • Fallback case (pinned cache binary temporarily renamed to simulate the corrupt-cache failure): POST /api/convert-pdfHTTP 200, valid 1-page PDF via /usr/bin/google-chrome. With the old code this exact scenario returned the "Could not find Chrome" failure.
  • Windows paths are behaviorally unchanged (same candidates as before, minus the dead hardcoded one); verified by review — no Windows machine was available for a live run.

findChromePath() only handled Windows, so on Linux/macOS it always
returned undefined and rendering depended entirely on puppeteer's
own cache download. When that cache is missing or corrupt (e.g. an
interrupted download leaves a version folder without the chrome
binary), PDF generation fails with:

  Failed to generate PDF: Could not find Chrome (ver. 145.0.7632.67)

- Remove hardcoded "C:\\Users\\Killian\\..." puppeteer-cache path
- Add PUPPETEER_EXECUTABLE_PATH as the first override
- Add puppeteer.executablePath() as a cache-aware candidate (guarded
  with try/catch so a broken cache no longer throws)
- Add Linux (/usr/bin/google-chrome, chromium, edge, snap, /opt) and
  macOS (/Applications/...) well-known locations
- Log every candidate path so misconfiguration is visible
Headings were rendered without id attributes, so markdown links
pointing at headers within the same document (e.g. a table of
contents using [Bab 1](Sterbweise#1-ringkasan-layanan)) had no target and
were dead in the generated PDF.

rehype-slug generates the same GitHub-style slugs users expect
(1-ringkasan-layanan, 13-bantuan-dan-kontak, ...), which Chrome's
print-to-PDF converts into named destinations and clickable
internal link annotations.
@arcahyadi

Copy link
Copy Markdown
Author

Follow-up commit `264d2d4` — second real-world failure found while rendering a 19 KB Indonesian student-services guide (12-page PDF with a linked table of contents):

Problem: internal anchor links ([Bab 5](#5-isian-setiap-jenis-surat)) were dead in the generated PDF. Root cause: the markdown pipeline never gave headings id attributes, so <a href="#5-isian-setiap-jenis-surat"> pointed at nothing. (Confirmed by running the exact plugin chain — output was <h2>5. Isian Setiap Jenis Surat</h2> with no id.)

Fix: added rehype-slug before rehype-highlight in app/lib/markdownParser.ts. It generates the same GitHub-style slugs users already expect from other markdown tools.

Verified end-to-end on the user's actual 12-page document:

  • Before: 0 named destinations in the PDF.
  • After: 13 named destinations (/1-ringkasan-layanan ... /13-bantuan-dan-kontak), 14 internal link annotations — all resolving to their targets — plus 8 external /URI links untouched.
  • tsc --noEmit clean.

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.

1 participant