can1357/oh-my-pi · error · ToolError
browser app.cdp_url must be the HTTP CDP discovery endpoint
Error message
browser app.cdp_url must be the HTTP CDP discovery endpoint (for example http://127.0.0.1:9222), not a ws:// browser websocket URL.
What it means
normalizeConnectedCdpUrl canonicalizes a user-supplied CDP URL from the browser app config and rejects ws:// or wss:// URLs. Chrome exposes two different endpoints: an HTTP discovery endpoint (e.g. http://127.0.0.1:9222) used with puppeteer's browserURL, and a raw browser websocket URL. This library requires the HTTP discovery endpoint and throws ToolError when given a websocket URL instead.
Source
Thrown at packages/coding-agent/src/tools/browser/registry.ts:157
// orphaned Chromium/app process / puppeteer handle survives to process
// exit. (Issue #3963.)
if (opts.signal?.aborted) {
await disposeBrowserHandle(handle, { kill: kind.kind === "spawned" }).catch(err => {
logger.debug("Failed to dispose orphan browser after abort", {
error: err instanceof Error ? err.message : String(err),
});
});
throw new ToolAbortError("Browser open aborted");
}
browsers.set(key, handle);
return handle;
}
}
export function normalizeConnectedCdpUrl(rawCdpUrl: string): string {
const cdpUrl = rawCdpUrl.replace(/\/+$/, "");
if (/^wss?:\/\//i.test(cdpUrl)) {
throw new ToolError(
"browser app.cdp_url must be the HTTP CDP discovery endpoint (for example http://127.0.0.1:9222), not a ws:// browser websocket URL.",
);
}
return cdpUrl;
}
async function openBrowserHandle(kind: BrowserKind, opts: AcquireBrowserOptions): Promise<BrowserHandle> {
if (kind.kind === "cmux") {
const client = new CmuxSocketClient({ socketPath: kind.socketPath, password: kind.password });
await client.connect();
return {
key: browserKey(kind),
kind,
client,
surface: kind.surface,
refCount: 0,
};
}View on GitHub (pinned to 9690622007)
Solutions
- Change app.cdp_url to the HTTP discovery endpoint, e.g. http://127.0.0.1:9222 (strip the /devtools/browser/... path entirely)
- If the browser runs on another host/port, point to http://<host>:<port> where the /json/version endpoint responds
- Restart the browser with --remote-debugging-port=<port> if no HTTP endpoint exists
Example fix
// before
{ "cdp_url": "ws://127.0.0.1:9222/devtools/browser/8f6a-..." }
// after
{ "cdp_url": "http://127.0.0.1:9222" } Defensive patterns
Strategy: validation
Validate before calling
function assertHttpCdpUrl(raw: string): string {
const trimmed = raw.replace(/\/+$/, "");
if (!/^https?:\/\//i.test(trimmed)) {
throw new Error(`cdp_url must start with http:// or https:// (got ${raw})`);
}
return trimmed;
} Type guard
function isHttpCdpUrl(raw: string): boolean {
return /^https?:\/\//i.test(raw.trim());
} Prevention
- Store the HTTP discovery endpoint (http://host:port) in browser app config, never webSocketDebuggerUrl
- Remember: ws:// URLs come from /json/version's webSocketDebuggerUrl — the tool wants the /json/version base URL instead
- Verify with `curl http://127.0.0.1:9222/json/version` that the configured endpoint is the HTTP one
- Document config examples using http:// to prevent copy-paste of ws:// from other guides
When it happens
Trigger: Setting browser app config cdp_url to a ws://... value, typically copied from the 'webSocketDebuggerUrl' field of /json/version output, or from a remote-debugging guide that shows the websocket URL.
Common situations: Users pasting the DevTools websocket URL from chrome://version or /json/version; misconfigured CI browser services that export ws endpoints; mixing up puppeteer.connect({browserWSEndpoint}) semantics with this tool's browserURL semantics.
Related errors
- app.path must be absolute (got ${JSON.stringify(exe)}). Pass
- OMP_AUTH_BROKER_ACCOUNT_POOL_FILE contains an empty provider
- OMP_AUTH_BROKER_ACCOUNT_POOL_FILE contains a provider id wit
- ${name} path does not exist: ${trimmed}
- Anthropic thinking budget requires max_tokens greater than $
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/fcf58d460e2adde0.
Report an issue: GitHub.