Skip to content

Commit 69d6d41

Browse files
authored
Merge pull request #48 from browser-use/fix/way2-fact-check-feedback
fix(connect): honor timeoutMs on profileDir poll; re-rank Way 2 docs (Chrome 147 DevToolsActivePort regression)
2 parents a17676f + aff79b8 commit 69d6d41

3 files changed

Lines changed: 50 additions & 25 deletions

File tree

packages/bcode-browser/skills/BROWSER.md

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,47 +31,52 @@ For this to work the user must have, **once**, navigated to `chrome://inspect/#r
3131
Failure modes and what they mean:
3232

3333
- **`connect()` throws "No running browser with remote debugging detected"** — the checkbox at `chrome://inspect/#remote-debugging` has not been ticked in any running Chrome profile, or no Chrome is running. Ask the user to open their target Chrome and tick the box.
34-
- **`connect()` throws with "403" / "permission" / "WS closed before open"** — the checkbox is ticked but the user hasn't clicked Allow on the popup yet. By default `connect()` errors fast (5s per candidate). To wait up to 30s for the click: pass `{ profileDir: "<abs path to user's profile>", timeoutMs: 30000 }`. Passing `profileDir` skips the OS scan and reads the WebSocket URL straight from `<profileDir>/DevToolsActivePort`works on every Chrome version including 144+ which doesn't serve `/json/version`.
34+
- **`connect()` throws with "403" / "permission" / "WS closed before open"** — the checkbox is ticked but the user hasn't clicked Allow on the popup yet. By default `connect()` errors fast (5s per candidate). To wait up to 30s for the click: pass `{ profileDir: "<abs path to user's profile>", timeoutMs: 30000 }`. Passing `profileDir` skips the OS scan and reads the WebSocket URL straight from `<profileDir>/DevToolsActivePort`. Note: this works for Way 1 (the user's existing profile) on every Chrome version including 144+. For Way 2 (a fresh profile launched with `--user-data-dir`), Chrome 147+ has been observed to not write this file — see Way 2 below for the `/json/version` route.
3535

3636
**Way 2 — connect to a Chrome you (or the user) launched with a debug port (isolated profile, no popups, ever).** Right choice for unattended automation, or whenever popup interruptions are unacceptable.
3737

38-
Launch Chrome with `--remote-debugging-port=<port> --user-data-dir=<path>`:
38+
Launch Chrome with `--remote-debugging-port=<port> --user-data-dir=<path>`. Pick any path the agent's tools can write to — a project-local directory like `./.bcode/way2-chrome` is a safe default; `/tmp/...` works wherever the sandbox allows it.
3939

4040
```bash
4141
# Linux
42-
google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/bcode-chrome
42+
google-chrome --remote-debugging-port=9222 --user-data-dir=./.bcode/way2-chrome
4343

4444
# macOS
4545
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
46-
--remote-debugging-port=9222 --user-data-dir=/tmp/bcode-chrome
46+
--remote-debugging-port=9222 --user-data-dir=./.bcode/way2-chrome
4747

4848
# Windows (cmd.exe)
4949
"C:\Program Files\Google\Chrome\Application\chrome.exe" ^
50-
--remote-debugging-port=9222 --user-data-dir=C:\bcode-chrome
50+
--remote-debugging-port=9222 --user-data-dir=.\.bcode\way2-chrome
5151

5252
# Windows (PowerShell)
5353
& "C:\Program Files\Google\Chrome\Application\chrome.exe" `
54-
--remote-debugging-port=9222 --user-data-dir=C:\bcode-chrome
54+
--remote-debugging-port=9222 --user-data-dir=.\.bcode\way2-chrome
5555
```
5656

57-
Then connect to it from a snippet — pass the same `--user-data-dir` value as `profileDir` and `connect()` reads the live WebSocket URL out of `<profileDir>/DevToolsActivePort`:
57+
Then resolve the live WebSocket URL via `/json/version` and connect:
5858

5959
```js
60-
await session.connect({ profileDir: "/tmp/bcode-chrome" }) // or "C:\\bcode-chrome" on Windows
60+
const ver = await fetch("http://127.0.0.1:9222/json/version").then(r => r.json())
61+
await session.connect({ wsUrl: ver.webSocketDebuggerUrl })
6162
```
6263
64+
This is the canonical Way 2 path. Works on every Chrome that serves `/json/version` (every Chromium-based browser launched with `--remote-debugging-port`).
65+
66+
**Older / alternate path: `{ profileDir }`.** On older Chrome (pre-147) and on the chrome://inspect Way 1 path, Chrome writes a `DevToolsActivePort` file inside the user-data-dir, and `session.connect({ profileDir: "<same path as --user-data-dir>" })` reads the WS URL directly from it — no HTTP probe. Chrome 147+ has been observed (macOS, Windows) to NOT write this file when launched with a custom `--user-data-dir`, so this path no longer works for Way 2 on modern Chrome. Use it only if `/json/version` is unavailable.
67+
6368
Two precisions on the `--user-data-dir`:
6469
6570
- **It must not be Chrome's platform default.** Chrome 136 and later silently no-op the `--remote-debugging-port` flag when `--user-data-dir` is the platform default, even if you pass it explicitly. The platform defaults are `%LOCALAPPDATA%\Google\Chrome\User Data` on Windows, `~/Library/Application Support/Google/Chrome` on macOS, `~/.config/google-chrome` on Linux. An empty or new path gives a fresh clean profile that Chrome will persist there across future launches.
6671
- **You cannot reuse the user's everyday Chrome profile by copying its files into a custom directory.** Chrome will accept the flag and start, so it looks like it works — but cookies are encrypted under a key bound to the *original* directory and will not survive the copy. Bookmarks and extensions transfer; logged-in sessions do not. If you need the user's real logins, use Way 1.
6772
68-
If you have a `wsUrl` directly (e.g. from `fetch("http://127.0.0.1:9222/json/version").then(r => r.json()).then(j => j.webSocketDebuggerUrl)`), you can also pass it as the escape hatch:
73+
The bare `ws://host:port/devtools/browser` form (no UUID suffix) does not work — Chrome's browser-level endpoint includes a per-process UUID. Always resolve via `/json/version` first.
6974
70-
```js
71-
await session.connect({ wsUrl: "ws://127.0.0.1:9222/devtools/browser/<uuid>" })
72-
```
75+
**Way 2 troubleshooting:**
7376
74-
The bare `ws://host:port/devtools/browser` form (no UUID suffix) does not work — Chrome's browser-level endpoint includes a per-process UUID. Prefer `{ profileDir }` unless you specifically need the WS URL form.
77+
- **Chrome's launch log prints `DevTools listening on ws://...:<port>/...` before the bind succeeds.** That line is not a reliable readiness signal: if the port is already taken, you'll see the line immediately followed by `bind() failed: Address already in use` and Chrome exits. Confirm the port is actually open with `curl http://127.0.0.1:<port>/json/version` (or fetch from a snippet) before connecting.
78+
- **Windows: launching Chrome while any other Chrome is already running silently hands the new flags off to the existing process**`--remote-debugging-port` is ignored. Kill all `chrome.exe` first (or use a unique `--user-data-dir` and accept that some Windows builds still no-op).
79+
- **`{ profileDir }` raises ENOENT on `DevToolsActivePort`** — Chrome 147+ doesn't write this file under custom `--user-data-dir`. Use the `/json/version` route above instead.
7580
7681
**Way 3 — provision and connect to a Browser Use cloud browser.** Best when the user can't see the browser, you need a clean profile, geo-located proxy, or fingerprint isolation. Read `{{SKILLS_DIR}}/cloud-browser.md` for the full pattern (provision, stop, swap profile/proxy). Briefly:
7782
@@ -82,9 +87,10 @@ const r = await fetch("https://api.browser-use.com/api/v3/browsers", {
8287
body: "{}",
8388
})
8489
const { id, cdpUrl, liveUrl } = await r.json()
85-
// BU's cdpUrl is the HTTP discovery endpoint (e.g. https://cdpN.browser-use.com),
90+
// BU's cdpUrl is the HTTPS discovery endpoint (e.g. https://cdpN.browser-use.com),
8691
// not a WebSocket URL. Resolve it like a remote Chrome: fetch /json/version and
87-
// use the webSocketDebuggerUrl field.
92+
// use the webSocketDebuggerUrl field. The resolved URL is `wss://...` (secure);
93+
// `session.connect({ wsUrl })` handles both `ws://` and `wss://` transparently.
8894
const ver = await fetch(`${cdpUrl}/json/version`).then(r => r.json())
8995
await session.connect({ wsUrl: ver.webSocketDebuggerUrl })
9096
console.log("liveUrl for the user to watch:", liveUrl)
@@ -196,5 +202,5 @@ Cache-bust (`?t=${Date.now()}`) is your responsibility: without it, edits to the
196202
- **`session.Page.navigate` hangs forever** → the page is showing a native dialog. Use `session.Page.handleJavaScriptDialog({ accept: true })` to dismiss.
197203
- **Selectors don't find elements that you can see** → likely an iframe or shadow DOM. Read `{{SKILLS_DIR}}/interaction-skills/iframes.md` or `shadow-dom.md`.
198204
- **Actions silently no-op** → the page is mid-load. After `Page.navigate`, await `session.waitFor("Page.loadEventFired")` before driving inputs.
199-
- **Connection refused, 403, or `WS closed before open` on connect()** → see the Way 1 failure-mode list above. Most often: the `chrome://inspect/#remote-debugging` checkbox isn't ticked, or the Chrome 144+ "Allow remote debugging?" popup hasn't been clicked. Pass `{ profileDir, timeoutMs: 30000 }` to wait up to 30s for the click, or fall back to Way 2.
205+
- **Connection refused, 403, or `WS closed before open` on connect()** → see the Way 1 failure-mode list above. Most often: the `chrome://inspect/#remote-debugging` checkbox isn't ticked, or the Chrome 144+ "Allow remote debugging?" popup hasn't been clicked. Pass `{ profileDir, timeoutMs: 30000 }` (Way 1, user's profile) to wait up to 30s for the click, or fall back to Way 2.
200206
- **Cloud `connect()` fails after a successful provision** → check that `cdp_url` came back in the POST response; some BU regions return `cdpUrl` (camelCase) — accept both. See `{{SKILLS_DIR}}/cloud-browser.md`.

packages/bcode-browser/skills/cloud-browser.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,18 @@ const r = await fetch("https://api.browser-use.com/api/v3/browsers", {
2929
// proxyCountryCode: "us", // geo-located proxy (default "us"; null disables)
3030
}),
3131
})
32+
// Successful provision returns 201, not 200 — `!r.ok` covers both.
3233
if (!r.ok) throw new Error(`provision failed: ${r.status} ${await r.text()}`)
33-
const { id, cdpUrl, liveUrl } = await r.json()
34+
const body = await r.json()
35+
const { id, cdpUrl, liveUrl } = body
3436
```
3537

38+
The response carries more than the three fields above. Other fields you may want:
39+
40+
- `timeoutAt` — ISO timestamp when BU will auto-reclaim the browser. Use it to schedule a `stop` or warn the user before quota expiry.
41+
- `recordingUrl` — playback URL for the session recording. Surface this to the user when handing back the run.
42+
- `status`, `startedAt`, `finishedAt`, `proxyUsedMb`, `proxyCost`, `browserCost`, `agentSessionId` — observability fields, not needed to drive the browser.
43+
3644
The `liveUrl` is a viewer URL the user can open in their own browser to watch the cloud browser's pixels. **Print it to console** so the user can click it:
3745

3846
```js
@@ -43,7 +51,7 @@ Stash `id` somewhere (a `globalThis.cloudBrowserId = id` is fine, or the snippet
4351

4452
## Connect
4553

46-
The `cdpUrl` from BU is an HTTP discovery endpoint (e.g. `https://cdpN.browser-use.com`), the same shape Chrome's `:9222` exposes locally, **not** a WebSocket URL. Resolve it via `/json/version`:
54+
The `cdpUrl` from BU is an HTTPS discovery endpoint (e.g. `https://cdpN.browser-use.com`), the same shape Chrome's `:9222` exposes locally, **not** a WebSocket URL. Resolve it via `/json/version`. The resolved URL is `wss://...` (secure WebSocket); `session.connect({ wsUrl })` handles `ws://` and `wss://` transparently, so the local-vs-cloud flow is identical from the snippet's perspective.
4755

4856
```js
4957
const ver = await fetch(`${cdpUrl}/json/version`).then(r => r.json())

packages/bcode-browser/src/cdp/session.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export class Session implements Transport {
8181
async connect(opts: ConnectOptions = {}): Promise<void> {
8282
const timeoutMs = opts.timeoutMs ?? 5_000;
8383
if (opts.wsUrl || opts.profileDir) {
84-
const wsUrl = await resolveWsUrl(opts);
84+
const wsUrl = await resolveWsUrl(opts, timeoutMs);
8585
await this.openWs(wsUrl, timeoutMs);
8686
return;
8787
}
@@ -268,10 +268,10 @@ function isBrowserLevel(method: string): boolean {
268268
* For auto-detect, call `session.connect()` with no args — it iterates
269269
* `detectBrowsers()` and picks the first browser whose WS accepts.
270270
*/
271-
export async function resolveWsUrl(opts: ConnectOptions): Promise<string> {
271+
export async function resolveWsUrl(opts: ConnectOptions, timeoutMs: number): Promise<string> {
272272
if (opts.wsUrl) return opts.wsUrl;
273273
if (opts.profileDir) {
274-
const { port, path } = await readDevToolsActivePort(opts.profileDir);
274+
const { port, path } = await readDevToolsActivePort(opts.profileDir, timeoutMs);
275275
return `ws://127.0.0.1:${port}${path}`;
276276
}
277277
throw new Error('resolveWsUrl needs { wsUrl } or { profileDir }. For auto-detect, call session.connect() directly.');
@@ -282,13 +282,19 @@ export async function resolveWsUrl(opts: ConnectOptions): Promise<string> {
282282
* line 1: port number
283283
* line 2: path (e.g. "/devtools/browser/<uuid>")
284284
* With both in hand we can build `ws://host:port<path>` with no HTTP probe.
285+
*
286+
* Note: Chrome 147+ has been observed to NOT write this file when launched
287+
* with a custom `--user-data-dir` (verified on macOS and Windows). For Way 2
288+
* with modern Chrome, prefer the `/json/version` -> wsUrl route instead.
285289
*/
286-
async function readDevToolsActivePort(profileDir: string): Promise<{ port: number; path: string }> {
287-
const deadline = Date.now() + 30_000;
290+
async function readDevToolsActivePort(profileDir: string, timeoutMs: number): Promise<{ port: number; path: string }> {
291+
const filePath = `${profileDir}/DevToolsActivePort`;
292+
const start = Date.now();
293+
const deadline = start + timeoutMs;
288294
let lastErr: unknown;
289295
while (Date.now() < deadline) {
290296
try {
291-
const text = (await Bun.file(`${profileDir}/DevToolsActivePort`).text()).trim();
297+
const text = (await Bun.file(filePath).text()).trim();
292298
const [portStr, path] = text.split('\n');
293299
const port = Number(portStr);
294300
if (!Number.isFinite(port)) throw new Error(`malformed port line: ${portStr}`);
@@ -302,7 +308,12 @@ async function readDevToolsActivePort(profileDir: string): Promise<{ port: numbe
302308
await Bun.sleep(250);
303309
}
304310
}
305-
throw new Error(`Could not read ${profileDir}/DevToolsActivePort after 30s: ${lastErr}`);
311+
const elapsed = Date.now() - start;
312+
throw new Error(
313+
`Polled ${filePath} for ${elapsed}ms (timeoutMs=${timeoutMs}): ${lastErr}. ` +
314+
`Note: Chrome 147+ may not write this file when launched with --user-data-dir. ` +
315+
`Try the /json/version fallback: fetch("http://127.0.0.1:<port>/json/version") -> webSocketDebuggerUrl, then session.connect({ wsUrl }).`,
316+
);
306317
}
307318

308319
/**

0 commit comments

Comments
 (0)