openclaw - 💡(How to fix) Fix Chrome CDP launch fails despite diagnostic confirming readiness (isChromeReachable vs diagnoseChromeCdp disagreement)

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

launchOpenClawChrome throws Failed to start Chrome CDP on port <port> for profile "<name>" while its own attached diagnostic confirms Chrome is reachable. The error message literally contains CDP diagnostic: ready after 67ms; cdp=http://127.0.0.1:18800; websocket=...; browser=Chrome/146... — i.e. the failure path's diagnostic says the system is healthy.

Reproduces intermittently on cold-start. The next call to the browser plugin succeeds immediately without restarting Chrome.

Error Message

launchOpenClawChrome throws Failed to start Chrome CDP on port <port> for profile "<name>" while its own attached diagnostic confirms Chrome is reachable. The error message literally contains CDP diagnostic: ready after 67ms; cdp=http://127.0.0.1:18800; websocket=...; browser=Chrome/146... — i.e. the failure path's diagnostic says the system is healthy. 3. The first invocation after a cold start (or after a recent gateway restart) intermittently fails with the error above. | isChromeReachable (line 1291) | HTTP /json/version only, with a 500ms AbortController timeout. Silently catches every error → returns false. | throw new Error(Failed to start Chrome CDP on port ${profile.cdpPort} for profile "${profile.name}". ${diagnosticText}${launchHints}${stderrHint}); When isChromeReachable returns false on its last call near the deadline (transient HTTP probe error, AbortController firing, or just narrow timing margin), the code throws. The diagnostic, fired immediately after, succeeds in 60-200ms because Chrome is in fact ready. The user sees a self-contradicting error and the launched Chrome process is then SIGKILLed (line 1428), so the next call has to launch all over again. log.warn(Removed stale Chromium Singleton* locks for profile "${profile.name}" and retrying launch.); throw new Error(Failed to start Chrome CDP on port ${profile.cdpPort} for profile "${profile.name}". ${diagnosticText}${launchHints}${stderrHint});

Root Cause

In dist/chrome-CP2x5dZ8.js (built from whatever the source file is — likely src/browser/chrome.ts), launchOpenClawChrome uses two different readiness criteria for the same Chrome instance:

FunctionWhat it checks
isChromeReachable (line 1291)HTTP /json/version only, with a 500ms AbortController timeout. Silently catches every error → returns false.
diagnoseChromeCdp (line 877)HTTP /json/version + WebSocket handshake + Browser.getVersion round-trip, with detailed failure codes.

The launch path uses the weaker probe to decide success/failure, but the stronger probe to format the diagnostic:

// line 1407-1430 (built JS)
const readyDeadline = Date.now() + (resolved.localLaunchTimeoutMs ?? CHROME_LAUNCH_READY_WINDOW_MS);
while (Date.now() < readyDeadline) {
    if (await isChromeReachable(profile.cdpUrl)) break;
    await new Promise((r) => setTimeout(r, 200));
}
if (!await isChromeReachable(profile.cdpUrl)) {       // <-- weak check decides failure
    const diagnosticText = await diagnoseChromeCdp(profile.cdpUrl)... // <-- strong check confirms ready
    ...
    throw new Error(`Failed to start Chrome CDP on port ${profile.cdpPort} for profile "${profile.name}". ${diagnosticText}${launchHints}${stderrHint}`);
}

When isChromeReachable returns false on its last call near the deadline (transient HTTP probe error, AbortController firing, or just narrow timing margin), the code throws. The diagnostic, fired immediately after, succeeds in 60-200ms because Chrome is in fact ready. The user sees a self-contradicting error and the launched Chrome process is then SIGKILLed (line 1428), so the next call has to launch all over again.

Fix Action

Fix / Workaround

Workaround in user code

Code Example

// line 1407-1430 (built JS)
const readyDeadline = Date.now() + (resolved.localLaunchTimeoutMs ?? CHROME_LAUNCH_READY_WINDOW_MS);
while (Date.now() < readyDeadline) {
    if (await isChromeReachable(profile.cdpUrl)) break;
    await new Promise((r) => setTimeout(r, 200));
}
if (!await isChromeReachable(profile.cdpUrl)) {       // <-- weak check decides failure
    const diagnosticText = await diagnoseChromeCdp(profile.cdpUrl)... // <-- strong check confirms ready
    ...
    throw new Error(`Failed to start Chrome CDP on port ${profile.cdpPort} for profile "${profile.name}". ${diagnosticText}${launchHints}${stderrHint}`);
}

---

const finalDiagnostic = await diagnoseChromeCdp(profile.cdpUrl).catch((err) => ({
    ok: false,
    cdpUrl: profile.cdpUrl,
    code: "diagnostic_threw",
    message: safeChromeCdpErrorMessage(err),
    elapsedMs: 0,
}));
if (!finalDiagnostic.ok) {
    const diagnosticText = formatChromeCdpDiagnostic(finalDiagnostic);
    const stderrOutput = normalizeOptionalString(Buffer.concat(stderrChunks).toString("utf8")) ?? "";
    if (allowSingletonRecovery && CHROME_SINGLETON_IN_USE_PATTERN.test(stderrOutput) && clearStaleChromeSingletonLocks(userDataDir)) {
        log.warn(`Removed stale Chromium Singleton* locks for profile "${profile.name}" and retrying launch.`);
        await terminateChromeForRetry(proc, userDataDir);
        return await launchOnceAndWait(false);
    }
    const stderrHint = stderrOutput ? `\nChrome stderr:\n${stderrOutput.slice(0, CHROME_STDERR_HINT_MAX_CHARS)}` : "";
    const launchHints = chromeLaunchHints({ stderrOutput, resolved, profile, launchOptions });
    try { proc.kill("SIGKILL"); } catch {}
    throw new Error(`Failed to start Chrome CDP on port ${profile.cdpPort} for profile "${profile.name}". ${diagnosticText}${launchHints}${stderrHint}`);
}
// Chrome is ready — proceed.
RAW_BUFFERClick to expand / collapse

Summary

launchOpenClawChrome throws Failed to start Chrome CDP on port <port> for profile "<name>" while its own attached diagnostic confirms Chrome is reachable. The error message literally contains CDP diagnostic: ready after 67ms; cdp=http://127.0.0.1:18800; websocket=...; browser=Chrome/146... — i.e. the failure path's diagnostic says the system is healthy.

Reproduces intermittently on cold-start. The next call to the browser plugin succeeds immediately without restarting Chrome.

Environment

  • OpenClaw 2026.5.12 (f066dd2)
  • macOS 26.0.0
  • Chrome 146.0.7680.165
  • Profile: managed openclaw profile (not host attach)

Reproduction

  1. Ensure no Chrome instance is running on port 18800.
  2. Run any browser command via the gateway, e.g. openclaw browser --timeout 25000 navigate "about:blank".
  3. The first invocation after a cold start (or after a recent gateway restart) intermittently fails with the error above.
  4. Re-running the exact same command within 1-2 seconds succeeds.

I hit this 5 times over 5 weeks (Apr 14, Apr 18, Apr 22, Apr 25, May 16), always on profile openclaw, always with the diagnostic reporting reachable. Reproduced again interactively just now while diagnosing it.

Root cause

In dist/chrome-CP2x5dZ8.js (built from whatever the source file is — likely src/browser/chrome.ts), launchOpenClawChrome uses two different readiness criteria for the same Chrome instance:

FunctionWhat it checks
isChromeReachable (line 1291)HTTP /json/version only, with a 500ms AbortController timeout. Silently catches every error → returns false.
diagnoseChromeCdp (line 877)HTTP /json/version + WebSocket handshake + Browser.getVersion round-trip, with detailed failure codes.

The launch path uses the weaker probe to decide success/failure, but the stronger probe to format the diagnostic:

// line 1407-1430 (built JS)
const readyDeadline = Date.now() + (resolved.localLaunchTimeoutMs ?? CHROME_LAUNCH_READY_WINDOW_MS);
while (Date.now() < readyDeadline) {
    if (await isChromeReachable(profile.cdpUrl)) break;
    await new Promise((r) => setTimeout(r, 200));
}
if (!await isChromeReachable(profile.cdpUrl)) {       // <-- weak check decides failure
    const diagnosticText = await diagnoseChromeCdp(profile.cdpUrl)... // <-- strong check confirms ready
    ...
    throw new Error(`Failed to start Chrome CDP on port ${profile.cdpPort} for profile "${profile.name}". ${diagnosticText}${launchHints}${stderrHint}`);
}

When isChromeReachable returns false on its last call near the deadline (transient HTTP probe error, AbortController firing, or just narrow timing margin), the code throws. The diagnostic, fired immediately after, succeeds in 60-200ms because Chrome is in fact ready. The user sees a self-contradicting error and the launched Chrome process is then SIGKILLed (line 1428), so the next call has to launch all over again.

Proposed fix

Use the same probe for both the decision and the diagnostic. isChromeCdpReady (defined at line 1322) already wraps diagnoseChromeCdp and returns its .ok — or capture the diagnostic once and use both its .ok and its formatted text:

const finalDiagnostic = await diagnoseChromeCdp(profile.cdpUrl).catch((err) => ({
    ok: false,
    cdpUrl: profile.cdpUrl,
    code: "diagnostic_threw",
    message: safeChromeCdpErrorMessage(err),
    elapsedMs: 0,
}));
if (!finalDiagnostic.ok) {
    const diagnosticText = formatChromeCdpDiagnostic(finalDiagnostic);
    const stderrOutput = normalizeOptionalString(Buffer.concat(stderrChunks).toString("utf8")) ?? "";
    if (allowSingletonRecovery && CHROME_SINGLETON_IN_USE_PATTERN.test(stderrOutput) && clearStaleChromeSingletonLocks(userDataDir)) {
        log.warn(`Removed stale Chromium Singleton* locks for profile "${profile.name}" and retrying launch.`);
        await terminateChromeForRetry(proc, userDataDir);
        return await launchOnceAndWait(false);
    }
    const stderrHint = stderrOutput ? `\nChrome stderr:\n${stderrOutput.slice(0, CHROME_STDERR_HINT_MAX_CHARS)}` : "";
    const launchHints = chromeLaunchHints({ stderrOutput, resolved, profile, launchOptions });
    try { proc.kill("SIGKILL"); } catch {}
    throw new Error(`Failed to start Chrome CDP on port ${profile.cdpPort} for profile "${profile.name}". ${diagnosticText}${launchHints}${stderrHint}`);
}
// Chrome is ready — proceed.

This:

  • Eliminates the contradiction (one probe makes the call).
  • Uses the stronger probe (HTTP + WS + Browser.getVersion), so we don't proceed if WS is broken even though /json/version answers.
  • Avoids the redundant second probe (we now call diagnose exactly once instead of isChromeReachable then diagnoseChromeCdp).

Suggested branch: fix/chrome-cdp-launch-readiness-uses-diagnostic.

Workaround in user code

Until fixed, catching Failed to start Chrome CDP in stderr and retrying once after ~3s clears it. Confirmed: the second attempt always succeeds because Chrome's already up.

Why this matters

Any cron-driven browser automation that fires once per day or per scheduled window cannot tolerate this failure — there's no human to retry. Mine sends an outbox alert and skips the day's run. Five missed runs in five weeks is from this single bug.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix Chrome CDP launch fails despite diagnostic confirming readiness (isChromeReachable vs diagnoseChromeCdp disagreement)