From 68ee3113a9fa97bb60963286ca4d509ee3ba077c Mon Sep 17 00:00:00 2001 From: Han Yang Date: Wed, 1 Apr 2026 09:09:31 +0800 Subject: [PATCH] Fix: CDP profiles prefer cdpPort over stale WebSocket cdpUrl (Resolves #58494) (#58499) * fix(browser): prefer cdpPort over stale WebSocket cdpUrl for attach-only profiles * fix(browser): preserve profile host when dropping stale devtools WS path --- extensions/browser/src/browser/config.test.ts | 37 +++++++++++++++++++ extensions/browser/src/browser/config.ts | 17 ++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/extensions/browser/src/browser/config.test.ts b/extensions/browser/src/browser/config.test.ts index e88a2aeba77..e305a3669a8 100644 --- a/extensions/browser/src/browser/config.test.ts +++ b/extensions/browser/src/browser/config.test.ts @@ -190,6 +190,43 @@ describe("browser config", () => { expect(profile?.cdpIsLoopback).toBe(true); }); + it("prefers cdpPort over stale WebSocket devtools cdpUrl when both are set", () => { + const resolved = resolveBrowserConfig({ + profiles: { + "chrome-cdp": { + cdpPort: 9222, + cdpUrl: "ws://127.0.0.1:9222/devtools/browser/old-stale-id", + attachOnly: true, + color: "#F59E0B", + }, + }, + }); + const profile = resolveProfile(resolved, "chrome-cdp"); + // cdpPort produces a stable HTTP endpoint; the stale WS session ID is dropped. + expect(profile?.cdpUrl).toBe("http://127.0.0.1:9222"); + expect(profile?.cdpPort).toBe(9222); + expect(profile?.cdpIsLoopback).toBe(true); + expect(profile?.attachOnly).toBe(true); + }); + + it("preserves profile host when dropping stale devtools WS path", () => { + const resolved = resolveBrowserConfig({ + cdpUrl: "http://devbox.local:9000", + profiles: { + "chrome-local": { + cdpPort: 9222, + cdpUrl: "ws://10.0.0.42:9222/devtools/browser/stale-id", + color: "#0066CC", + }, + }, + }); + const profile = resolveProfile(resolved, "chrome-local"); + // Host comes from the profile WS URL, not the global cdpUrl. + expect(profile?.cdpUrl).toBe("http://10.0.0.42:9222"); + expect(profile?.cdpHost).toBe("10.0.0.42"); + expect(profile?.cdpIsLoopback).toBe(false); + }); + it("rejects unsupported protocols", () => { expect(() => resolveBrowserConfig({ cdpUrl: "ftp://127.0.0.1:18791" })).toThrow( "must be http(s) or ws(s)", diff --git a/extensions/browser/src/browser/config.ts b/extensions/browser/src/browser/config.ts index a5bc131766a..4b72d8aef80 100644 --- a/extensions/browser/src/browser/config.ts +++ b/extensions/browser/src/browser/config.ts @@ -337,7 +337,22 @@ export function resolveProfile( }; } - if (rawProfileUrl) { + // When both cdpPort and cdpUrl are set and the URL contains a + // /devtools/browser/ path (a session-specific WebSocket ID), prefer + // cdpPort — the HTTP endpoint is stable across Chrome restarts while the + // WS path goes stale. For cloud CDP services (e.g. Browserbase) that + // use WSS URLs without a devtools path, preserve the full URL. + const hasStaleWsPath = + rawProfileUrl !== "" && + cdpPort > 0 && + /^wss?:\/\//i.test(rawProfileUrl) && + /\/devtools\/browser\//i.test(rawProfileUrl); + + if (hasStaleWsPath) { + const parsed = new URL(rawProfileUrl); + cdpHost = parsed.hostname; + cdpUrl = `${resolved.cdpProtocol}://${cdpHost}:${cdpPort}`; + } else if (rawProfileUrl) { const parsed = parseHttpUrl(rawProfileUrl, `browser.profiles.${profileName}.cdpUrl`); cdpHost = parsed.parsed.hostname; cdpPort = parsed.port;