BigPizzaV3/CodexPlusPlus · error · anyhow::Error

browser WebSocket URL has no Browser ID

Error message

browser WebSocket URL has no Browser ID

What it means

Thrown by `browser_id` when a Chromium DevTools (CDP) browser WebSocket URL does not contain a path segment shaped like `/devtools/browser/<id>`. The library only accepts DevTools browser endpoints whose final path segment is a non-empty Browser ID, so it can extract and return the browser identifier.

Solutions

  1. Use the browser-level webSocketDebuggerUrl from the `/json/version` endpoint (it ends in /devtools/browser/<id>), not a page target URL
  2. Verify the URL has a non-empty final path segment: print `url.path_segments()` before calling
  3. Re-fetch the endpoint from a running browser rather than caching a stale URL string

Example fix

// before
let id = cdp::browser_id("ws://127.0.0.1:9222/devtools/page/ABC")?;
// after
let version: serde_json::Value = reqwest::get("http://127.0.0.1:9222/json/version").await?.json().await?;
let ws_url = version["webSocketDebuggerUrl"].as_str().unwrap();
let id = cdp::browser_id(ws_url)?;
Defensive patterns

Strategy: validation

Validate before calling

fn looks_like_browser_ws_url(url: &str) -> bool {
    reqwest::Url::parse(url).ok()
        .and_then(|u| u.path_segments().map(|s| s.collect::<Vec<_>>()))
        .map(|segs| segs.len() >= 3 && segs[0] == "devtools" && segs[1] == "browser" && !segs[2].is_empty())
        .unwrap_or(false)
}

Type guard

fn is_browser_ws_url(u: &reqwest::Url) -> bool {
    u.scheme().starts_with("ws")
        && u.path_segments().map(|mut s| {
            s.next() == Some("devtools")
                && s.next() == Some("browser")
                && s.next().map_or(false, |id| !id.is_empty())
        }).unwrap_or(false)
}

Prevention

When it happens

Trigger: Calling `browser_id` with a URL whose path is missing (e.g. `ws://127.0.0.1:9222`), malformed, or whose segments are not exactly `devtools`, `browser`, `<non-empty id>` — e.g. a page-level `/devtools/page/<id>` URL or an empty ID.

Common situations: Passing a page target's webSocketDebuggerUrl instead of the browser-level one, the browser returning a URL without an ID (older/patched Chromium builds, proxies stripping path segments), or a truncated/hand-edited endpoint string.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/59789ff9ff506b66. Report an issue: GitHub.

Appendix: source

Thrown at crates/codex-plus-core/src/cdp.rs:41

#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct CdpBrowserIdentity {
    #[serde(rename = "Browser")]
    pub browser: String,
    #[serde(rename = "webSocketDebuggerUrl")]
    pub web_socket_debugger_url: String,
}

impl CdpBrowserIdentity {
    pub fn browser_id(&self) -> anyhow::Result<String> {
        let url = reqwest::Url::parse(&self.web_socket_debugger_url)
            .context("invalid browser WebSocket URL")?;
        let mut segments = url
            .path_segments()
            .ok_or_else(|| anyhow::anyhow!("browser WebSocket URL has no path"))?;
        match (segments.next(), segments.next(), segments.next()) {
            (Some("devtools"), Some("browser"), Some(id)) if !id.is_empty() => Ok(id.to_string()),
            _ => bail!("browser WebSocket URL has no Browser ID"),
        }
    }
}

/// Returns whether the requested loopback port exposes a CDP target list.
pub(crate) fn endpoint_available(debug_port: u16) -> bool {
    [
        SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), debug_port),
        SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), debug_port),
    ]
    .into_iter()
    .any(|address| probe_endpoint(address, debug_port))
}

fn probe_endpoint(address: SocketAddr, debug_port: u16) -> bool {
    let Ok(mut stream) = TcpStream::connect_timeout(&address, CDP_PROBE_TIMEOUT) else {
        return false;
    };

View on GitHub (pinned to b1ed92e5e4)