Skip to content

fix(browser): fall back to direct CDP connection for remote profiles#15845

Closed
kushagrob wants to merge 1 commit intoopenclaw:mainfrom
kushagrob:fix/remote-cdp-preflight-fallback
Closed

fix(browser): fall back to direct CDP connection for remote profiles#15845
kushagrob wants to merge 1 commit intoopenclaw:mainfrom
kushagrob:fix/remote-cdp-preflight-fallback

Conversation

@kushagrob
Copy link
Copy Markdown

@kushagrob kushagrob commented Feb 13, 2026

Summary

Remote CDP providers like Browserbase don't serve the /json/version HTTP endpoint that ensureBrowserAvailable() uses as a pre-flight check. This causes all remote CDP connections to these providers to fail with "Remote CDP is not reachable" even though the browser is fully available via WebSocket/CDP.

connectBrowser() in pw-session.ts already handles this case gracefully — it falls back to using the raw cdpUrl when getChromeWebSocketUrl() returns null:

const wsUrl = await getChromeWebSocketUrl(normalized, timeout).catch(() => null);
const endpoint = wsUrl ?? normalized;

But ensureBrowserAvailable() throws before connectBrowser() is ever called.

Fix

  • In the !httpReachable && remoteCdp branch of ensureBrowserAvailable(), try a direct Playwright connectOverCDP via listPagesViaPlaywright() before throwing
  • This follows the existing pattern already used by listTabs() for remote profiles
  • Only affects remote CDP profiles where the HTTP pre-flight fails — no change to local profiles, extension relay, or attachOnly behavior
  • If the direct CDP connection also fails, falls through to the existing error

Affected providers

This fix unblocks remote CDP providers that use WebSocket-only connection protocols:

  • Browserbase (wss://connect.usw2.browserbase.com/?signingKey=<JWE>)
  • Steel (similar WebSocket-only CDP)
  • Any custom CDP endpoint that doesn't implement Chrome DevTools HTTP endpoints

Test plan

  • Verify local Chrome profiles still work (no regression — httpReachable=true path unchanged)
  • Verify extension relay profiles still work (separate code path, untouched)
  • Verify remote CDP with /json/version support (e.g., Browserless) still works via fast HTTP path
  • Verify remote CDP without /json/version (e.g., Browserbase) now connects via fallback
  • Verify that unreachable remote CDP still throws appropriate error

Fixes #15582
Related: #5690

Greptile Overview

Greptile Summary

This change updates ensureBrowserAvailable() to handle remote CDP providers that don’t expose Chrome’s /json/version HTTP endpoint by attempting a direct Playwright connectOverCDP (via listPagesViaPlaywright) before throwing “Remote CDP is not reachable”. This aligns the pre-flight behavior with existing remote-profile tab listing logic that already uses Playwright for remote CDP.

Key concern: the new fallback path can create a long-lived, globally cached Playwright CDP connection (and uses multi-retry 5s+ timeouts), which turns a reachability probe into a stateful and potentially slow operation. Also, errors from the fallback are swallowed, making failures harder to debug when the fallback is exercised.

Confidence Score: 3/5

  • This PR is likely safe but introduces a meaningful behavioral change in preflight checks that could impact latency and resource usage for remote CDP profiles.
  • Core logic change is small and scoped to remoteCdp && !httpReachable, but it now establishes a cached Playwright CDP connection during ensureBrowserAvailable() and may block for connect retries/timeouts; the fallback also hides the underlying connection error, reducing debuggability on failure paths.
  • src/browser/server-context.ts (remote CDP fallback in ensureBrowserAvailable), src/browser/pw-session.ts (connectBrowser caching/timeouts interplay)

Last reviewed commit: 1bb9284

(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!

Remote CDP providers like Browserbase don't serve the /json/version
HTTP endpoint that ensureBrowserAvailable() uses as a pre-flight check.
This causes all remote CDP connections to fail with "Remote CDP is not
reachable" even though the browser is fully available via WebSocket.

connectBrowser() already handles this case gracefully — it falls back
to using the raw cdpUrl when getChromeWebSocketUrl() returns null. But
ensureBrowserAvailable() throws before connectBrowser() is ever called.

This fix adds a direct Playwright connectOverCDP fallback in the
!httpReachable && remoteCdp path, using the existing
listPagesViaPlaywright() pattern already used by listTabs() for remote
profiles. If the direct connection succeeds, the browser is available
and execution continues normally.

Fixes openclaw#15582
Related: openclaw#5690

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@kushagrob kushagrob closed this Feb 13, 2026
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +317 to +330
// Remote CDP providers (e.g., Browserbase) may not serve /json/version
// but are still reachable via direct Playwright connectOverCDP.
// Try a direct connection as a fallback before giving up.
if (remoteCdp) {
try {
const mod = await getPwAiModule({ mode: "strict" });
const fn = (mod as Partial<PwAiModule> | null)?.listPagesViaPlaywright;
if (typeof fn === "function") {
await fn({ cdpUrl: profile.cdpUrl });
return;
}
} catch {
// Direct CDP connection also failed — fall through to throw
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probe creates cached CDP

This fallback calls listPagesViaPlaywright(), which establishes (and globally caches) a Playwright chromium.connectOverCDP connection (src/browser/pw-session.ts:320-366). That means ensureBrowserAvailable() can now create a long-lived CDP connection on remoteCdp && !httpReachable, and it also inherits connectBrowser()’s multi-retry 5s/7s/9s timeouts (src/browser/pw-session.ts:331-353), turning what used to be a fast reachability failure into a potentially long, stateful side-effect. Consider using a non-cached / short-timeout connectivity check here, or explicitly ensuring any connection created for this probe is closed/doesn’t populate the shared cache.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/browser/server-context.ts
Line: 317:330

Comment:
**Probe creates cached CDP**

This fallback calls `listPagesViaPlaywright()`, which establishes (and globally caches) a Playwright `chromium.connectOverCDP` connection (`src/browser/pw-session.ts:320-366`). That means `ensureBrowserAvailable()` can now create a long-lived CDP connection on `remoteCdp && !httpReachable`, and it also inherits `connectBrowser()`’s multi-retry 5s/7s/9s timeouts (`src/browser/pw-session.ts:331-353`), turning what used to be a fast reachability failure into a potentially long, stateful side-effect. Consider using a non-cached / short-timeout connectivity check here, or explicitly ensuring any connection created for this probe is closed/doesn’t populate the shared cache.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +321 to +331
try {
const mod = await getPwAiModule({ mode: "strict" });
const fn = (mod as Partial<PwAiModule> | null)?.listPagesViaPlaywright;
if (typeof fn === "function") {
await fn({ cdpUrl: profile.cdpUrl });
return;
}
} catch {
// Direct CDP connection also failed — fall through to throw
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lost fallback error context

The catch {} here swallows the real failure reason from listPagesViaPlaywright() (auth/TLS/DNS/protocol issues) and then falls through to throw the generic “Remote CDP … not reachable” message. When this fallback path is hit, it will be much harder to diagnose why the direct CDP attempt failed. At minimum, consider including the caught error message (or chaining via new Error(msg, { cause: err })) in the thrown error for the remoteCdp case.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/browser/server-context.ts
Line: 321:331

Comment:
**Lost fallback error context**

The `catch {}` here swallows the real failure reason from `listPagesViaPlaywright()` (auth/TLS/DNS/protocol issues) and then falls through to throw the generic “Remote CDP … not reachable” message. When this fallback path is hit, it will be much harder to diagnose why the direct CDP attempt failed. At minimum, consider including the caught error message (or chaining via `new Error(msg, { cause: err })`) in the thrown error for the `remoteCdp` case.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Remote CDP (Browserless) blocked with “Port is in use … but not by openclaw” (OpenClaw 2026.2.12)

1 participant