#2108

Slow liveness-probe reply makes the driver redial while still attached, wedging single-attachment CDP endpoints

Openenckequityopened 17d ago

Summary

With --cdp <ws-url> against an endpoint that allows only one attached client, a single slow reply to the liveness probe leaves the daemon unusable. is_connection_alive gives up after a fixed 3 s (cli/src/native/browser.rs), so handle_launch treats the connection as dead and dials a second websocket while the first is still open. The endpoint refuses it (403 Forbidden). The command fails, and every later command fails the same way until the daemon is killed.

This is related to #1726 (sockets never closed on redial). That fix is in 0.39.0, but the redial itself still happens while the old socket is open.

Repro (v0.39.0, macOS arm64, no browser needed)

The script below is a loopback CDP server that refuses a second websocket while one is open and answers Browser.getVersion after 3.2 s when asked.

npm pack agent-browser@0.39.0 && tar xzf agent-browser-0.39.0.tgz package/bin
uv run --with websockets python repro.py "$PWD/package/bin/agent-browser-darwin-arm64"

Output:

1 normal  : True
2 slow    : False CDP WebSocket connect failed: HTTP error: 403 Forbidden
3 after   : False CDP WebSocket connect failed: HTTP error: 403 Forbidden
repro.py
# Minimal loopback CDP server that, like a single-attachment gateway, refuses a second
# websocket while one is open, and answers Browser.getVersion after 3.2 s.
# usage: python repro.py /path/to/agent-browser
import asyncio, json, os, sys, tempfile
from http import HTTPStatus
from websockets.asyncio.server import serve
from websockets.exceptions import ConnectionClosed

BIN, open_count, slow = sys.argv[1], 0, False

async def gate(conn, req):
    if open_count: return conn.respond(HTTPStatus.FORBIDDEN, "already attached")

async def handler(ws):
    global open_count
    open_count += 1
    try:
        async for raw in ws:
            m = json.loads(raw)
            if m["method"] == "Browser.getVersion" and slow: await asyncio.sleep(3.2)
            res = {}
            if m["method"] == "Target.getTargets":
                res = {"targetInfos": [{"targetId": "p", "type": "page", "title": "t", "url": "https://example.com/", "attached": False}]}
            elif m["method"] == "Target.attachToTarget": res = {"sessionId": "s"}
            elif m["method"] == "Runtime.evaluate": res = {"result": {"type": "string", "value": "https://example.com/"}}
            elif m["method"] == "Browser.getVersion": res = {"protocolVersion": "1.3", "product": "Chrome/1", "revision": "r", "userAgent": "u", "jsVersion": "v"}
            await ws.send(json.dumps({"id": m["id"], "result": res}))
    except ConnectionClosed: pass
    finally: open_count -= 1

async def main():
    global slow
    d = tempfile.mkdtemp()
    env = {"PATH": "/usr/bin:/bin", "HOME": d, "TMPDIR": d, "AGENT_BROWSER_SESSION": "repro", "AGENT_BROWSER_SOCKET_DIR": d}
    async with serve(handler, "127.0.0.1", 0, process_request=gate) as srv:
        port = srv.sockets[0].getsockname()[1]
        async def run(*a):
            p = await asyncio.create_subprocess_exec(BIN, "--cdp", f"ws://127.0.0.1:{port}/devtools/browser/x", "--json", *a,
                env=env, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
            out, _ = await asyncio.wait_for(p.communicate(), 30)
            return json.loads(out)
        print("1 normal  :", (await run("get", "url"))["success"])
        slow = True
        r = await run("get", "url"); print("2 slow    :", r["success"], r.get("error"))
        slow = False
        r = await run("get", "url"); print("3 after   :", r["success"], r.get("error"))
        os.system("pkill -f 'agent-browser.*repro' >/dev/null 2>&1")
asyncio.run(main())

Expected

A slow but healthy connection should not cause a second attachment. Commands 2 and 3 should succeed.

Suggested fix

  1. Make the probe timeout configurable (for example AGENT_BROWSER_LIVENESS_TIMEOUT_MS, default 3000) so callers with slow remote browsers can raise it.
  2. When the probe fails on an external CDP connection, close the existing connection before redialing, or refuse to redial while it is open and return a clear error. Do not attach a second client while the first is still open.

Happy to send a PR for 1 if that direction is acceptable.

Activity

0

No activity yet.