{"record":{"id":"022373cb4ff53a18","repo":"BigPizzaV3/CodexPlusPlus","slug":"browser-websocket-url-has-no-path","errorCode":null,"errorMessage":"browser WebSocket URL has no path","messagePattern":"browser WebSocket URL has no path","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/cdp.rs","lineNumber":38,"sourceCode":"    #[serde(default, rename = \"webSocketDebuggerUrl\")]\n    pub web_socket_debugger_url: Option<String>,\n}\n\n#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]\npub struct CdpBrowserIdentity {\n    #[serde(rename = \"Browser\")]\n    pub browser: String,\n    #[serde(rename = \"webSocketDebuggerUrl\")]\n    pub web_socket_debugger_url: String,\n}\n\nimpl CdpBrowserIdentity {\n    pub fn browser_id(&self) -> anyhow::Result<String> {\n        let url = reqwest::Url::parse(&self.web_socket_debugger_url)\n            .context(\"invalid browser WebSocket URL\")?;\n        let mut segments = url\n            .path_segments()\n            .ok_or_else(|| anyhow::anyhow!(\"browser WebSocket URL has no path\"))?;\n        match (segments.next(), segments.next(), segments.next()) {\n            (Some(\"devtools\"), Some(\"browser\"), Some(id)) if !id.is_empty() => Ok(id.to_string()),\n            _ => bail!(\"browser WebSocket URL has no Browser ID\"),\n        }\n    }\n}\n\n/// Returns whether the requested loopback port exposes a CDP target list.\npub(crate) fn endpoint_available(debug_port: u16) -> bool {\n    [\n        SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), debug_port),\n        SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), debug_port),\n    ]\n    .into_iter()\n    .any(|address| probe_endpoint(address, debug_port))\n}\n\nfn probe_endpoint(address: SocketAddr, debug_port: u16) -> bool {","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/1f431ae49b57b3055e0e6845ba6156c6b4232b4d/crates/codex-plus-core/src/cdp.rs#L20-L56","documentation":"CdpBrowserIdentity::browser_id (crates/codex-plus-core/src/cdp.rs:38) derives the browser ID from the path of the /json/version webSocketDebuggerUrl and expects the shape /devtools/browser/<id>. path_segments() returns None only for URLs that cannot be a base (opaque-path URLs, e.g. 'ws:devtools' or a bare non-hierarchical string that still parses), so this error indicates a malformed, non-hierarchical debugger URL rather than a merely empty path.","triggerScenarios":"Calling browser_id() on a CdpBrowserIdentity whose web_socket_debugger_url parses via Url::parse but has no hierarchical path — hand-built strings like \"ws:browser\" or data produced by a non-Chromium endpoint that mimics /json/version with an opaque ws URL.","commonSituations":"A mock/stub CDP endpoint used in tests returning a shorthand URL; a browser fork or other DevTools-protocol server whose version payload differs from Chrome's; string munging (schema prefix stripping) that turns ws://host/devtools/... into an opaque form.","solutions":["Use the webSocketDebuggerUrl exactly as returned by the real endpoint's /json/version — do not rewrite it","Validate the URL shape (scheme ws/wss + host + /devtools/browser/<id> path) before constructing CdpBrowserIdentity or calling browser_id()","If you control the server, return a full hierarchical URL: ws://127.0.0.1:9222/devtools/browser/<uuid>"],"exampleFix":"// before: opaque URL — Url::parse succeeds, path_segments() is None\nlet id = identity.browser_id()?; // web_socket_debugger_url = \"ws:browser\"\n\n// after: full hierarchical URL from /json/version\n// web_socket_debugger_url = \"ws://127.0.0.1:9222/devtools/browser/<uuid>\"\nlet id = identity.browser_id()?; // Ok(\"<uuid>\")","handlingStrategy":"validation","validationCode":"// Require the /devtools/browser/<id> shape before deriving a browser id\nlet u = reqwest::Url::parse(&identity.web_socket_debugger_url)?;\nensure!(u.path_segments().is_some(), \"URL has no hierarchical path\");\nensure!(u.path().starts_with(\"/devtools/browser/\"), \"unexpected CDP path shape\");\nlet id = identity.browser_id()?;","typeGuard":"fn is_hierarchical_ws_url(url: &str) -> bool {\n    reqwest::Url::parse(url)\n        .ok()\n        .and_then(|u| u.path_segments().map(|mut s| s.next().is_some()))\n        .unwrap_or(false)\n}","tryCatchPattern":"match identity.browser_id() {\n    Ok(id) => Ok(id),\n    Err(e) if e.to_string().contains(\"no path\") || e.to_string().contains(\"Browser ID\") => {\n        // discard the malformed identity and re-fetch /json/version from the endpoint\n        refetch_browser_identity(port).await?.browser_id()\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Never rewrite or shorten webSocketDebuggerUrl strings","In tests/mocks, return full hierarchical URLs (ws://127.0.0.1:<port>/devtools/browser/<uuid>)","Validate the URL shape at the trust boundary before persisting identities"],"tags":["cdp","url-parsing","browser-identity"],"backgroundTag":"malformed-websocket-url","analyzedSha":"1f431ae49b57b3055e0e6845ba6156c6b4232b4d","analyzedAt":"2026-08-16T20:54:18.598Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}