From 396202eca60569bca19d25d08e233340bff40619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sun, 29 Mar 2026 11:59:18 +0000 Subject: [PATCH] fix(engine): fall back to screenshot mode when system Chrome is not headless-shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The engine assumed any binary passed via PRODUCER_HEADLESS_SHELL_PATH supported the HeadlessExperimental.beginFrame CDP command. When the CLI resolved system Chrome (e.g. /usr/bin/google-chrome) instead of chrome-headless-shell, the render would silently hang for 120s then timeout — the #1 new-user friction point. Now checks the binary path for "chrome-headless-shell" before selecting beginframe capture mode. System Chrome falls back to screenshot mode which works universally. Reproducer: # On a machine with system Chrome but no chrome-headless-shell cached npx hyperframes init test --template blank --non-interactive cd test && npx hyperframes render --output out.mp4 # Was: 120s hang, then "Timed out after waiting 120000ms" # Now: renders successfully via screenshot mode --- packages/cli/src/browser/manager.ts | 19 +++++++++++++++---- .../engine/src/services/browserManager.ts | 10 ++++++++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/browser/manager.ts b/packages/cli/src/browser/manager.ts index 4ca5a4ab..eccbf162 100644 --- a/packages/cli/src/browser/manager.ts +++ b/packages/cli/src/browser/manager.ts @@ -107,12 +107,23 @@ export async function findBrowser(): Promise { } /** - * Find or download a browser. - * Resolution: env var -> cached download -> system Chrome -> auto-download. + * Find or download a browser suitable for rendering. + * Resolution: env var -> cached headless-shell -> auto-download. + * + * System Chrome is NOT used for rendering because it does not support + * the HeadlessExperimental.beginFrame CDP command required for + * deterministic frame capture, and older system Chrome versions are + * often incompatible with the version of puppeteer-core bundled in + * the engine (protocol mismatch → silent 120s timeout). */ export async function ensureBrowser(options?: EnsureBrowserOptions): Promise { - const existing = await findBrowser(); - if (existing) return existing; + // Env override and cached headless-shell are trusted. + // System Chrome is skipped — it causes silent render timeouts. + const fromEnv = findFromEnv(); + if (fromEnv) return fromEnv; + + const fromCache = await findFromCache(); + if (fromCache) return fromCache; const platform = detectBrowserPlatform(); if (!platform) { diff --git a/packages/engine/src/services/browserManager.ts b/packages/engine/src/services/browserManager.ts index 1262f1c7..03fd1bae 100644 --- a/packages/engine/src/services/browserManager.ts +++ b/packages/engine/src/services/browserManager.ts @@ -97,16 +97,22 @@ export async function acquireBrowser( const headlessShell = resolveHeadlessShellPath(config); // BeginFrame requires chrome-headless-shell AND Linux (crashes on macOS/Windows). + // System Chrome (google-chrome, chromium) does NOT support the + // HeadlessExperimental.beginFrame CDP command — only the dedicated + // chrome-headless-shell binary does. Passing system Chrome as the + // executable causes a 120-second silent hang followed by a timeout. const isLinux = process.platform === "linux"; const forceScreenshot = config?.forceScreenshot ?? DEFAULT_CONFIG.forceScreenshot; + const isHeadlessShellBinary = headlessShell ? /chrome-headless-shell/.test(headlessShell) : false; let captureMode: CaptureMode; let executablePath: string | undefined; - if (headlessShell && isLinux && !forceScreenshot) { + if (headlessShell && isLinux && isHeadlessShellBinary && !forceScreenshot) { captureMode = "beginframe"; executablePath = headlessShell; } else { - // Screenshot mode with renderSeek: works on all platforms. + // Screenshot mode with renderSeek: works on all platforms, and + // as a fallback when system Chrome is the only available binary. captureMode = "screenshot"; executablePath = headlessShell ?? undefined; }