diff --git a/packages/cli/src/browser/manager.ts b/packages/cli/src/browser/manager.ts index 4ca5a4ab4..eccbf162d 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 1262f1c75..03fd1baec 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; }