Skip to content

CDP: browser.close() kills external Chrome when connectViaCDP validation fails #539

@RussellZager

Description

@RussellZager

Bug Summary

agent-browser --cdp 9222 open <url> kills the external Chrome process instead of gracefully disconnecting when the CDP connection validation fails.

Environment

  • agent-browser: 0.14.0
  • Chrome Canary: 147.0.7701.0
  • macOS, ARM64
  • playwright-core (bundled)

Reproduction

  1. Start Chrome with remote debugging:
    /Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --remote-debugging-port=9222
  2. Verify CDP is active:
    curl -s http://localhost:9222/json/version  # returns valid JSON
  3. Run:
    agent-browser --cdp 9222 open https://example.com
  4. Result: Chrome Canary process is killed. curl -s http://localhost:9222/json/version returns empty.

Contrast: agent-browser --cdp 9222 tab new https://example.com works correctly without killing Chrome.

Root Cause

Two locations in dist/browser.js call browser.close() on a Playwright Browser object obtained via chromium.connectOverCDP(). Playwright's connectOverCDP() does not set _shouldCloseConnectionOnClose = true (unlike connect()), so .close() sends the CDP Browser.close command which terminates the browser process.

Location 1: connectViaCDP error handler (line ~1202-1205)

catch (error) {
    // Clean up browser connection if validation or setup failed
    await browser.close().catch(() => { });  // <-- kills Chrome!
    throw error;
}

If anything throws during page validation (e.g., no pages with non-empty URLs, setupPageTracking failure), this catch block runs browser.close() which sends Browser.close via CDP → Chrome dies.

Location 2: BrowserManager.close() (line ~1990-1995)

else if (this.cdpEndpoint !== null) {
    // CDP: only disconnect, don't close external app's pages
    if (this.browser) {
        await this.browser.close().catch(() => { });  // <-- also kills Chrome!
    }
}

The comment explicitly says "only disconnect, don't close external app's pages" but .close() does the opposite for CDP connections.

Playwright behavior reference

Connection method _shouldCloseConnectionOnClose .close() behavior
chromium.connect() true (browserType.js:145) Drops WebSocket only
chromium.connectOverCDP() false (default) Sends Browser.close CDP command → kills process

Suggested Fix

Replace browser.close() with a disconnect-only call at both locations. Options:

  1. Preferred: Use browser.disconnect() if available, or access the underlying connection:

    // In connectViaCDP catch block:
    await browser._connection?.close();
    
    // In BrowserManager.close() CDP branch:
    await browser._connection?.close();
  2. Alternative: Set the flag after connecting:

    const browser = await chromium.connectOverCDP(cdpUrl, { timeout: options?.timeout });
    browser._shouldCloseConnectionOnClose = true;  // Ensure close() only disconnects
  3. Simplest: Just don't call close in the error handler — let the WebSocket connection be garbage-collected:

    catch (error) {
        // Don't call browser.close() — it kills external Chrome via CDP
        throw error;
    }

Why tab new works

tab new <url> runs after connectViaCDP has already succeeded (the try block completed). It creates a new page via context.newPage() + page.goto() — no risk of hitting the catch block with browser.close().

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions