{"record":{"id":"7166be0b5220d20c","repo":"BigPizzaV3/CodexPlusPlus","slug":"selected-cdp-target-has-no-websocket-url","errorCode":null,"errorMessage":"selected CDP target has no websocket URL","messagePattern":"selected CDP target has no websocket URL","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/launcher.rs","lineNumber":2479,"sourceCode":"}\n\nasync fn run_bridge_reinjector(\n    bridge_reinjector: Option<BridgeReinjector>,\n    default_reinjector: BridgeReinjector,\n) -> anyhow::Result<()> {\n    match bridge_reinjector {\n        Some(reinject) => reinject().await,\n        None => default_reinjector().await,\n    }\n}\n\nasync fn bridge_health_ok(debug_port: u16) -> anyhow::Result<bool> {\n    let targets = crate::cdp::list_targets(debug_port).await?;\n    let target = crate::cdp::pick_injectable_codex_page_target(&targets)?;\n    let websocket_url = target\n        .web_socket_debugger_url\n        .as_deref()\n        .ok_or_else(|| anyhow::anyhow!(\"selected CDP target has no websocket URL\"))?;\n    let result = crate::bridge::evaluate_script_with_await_promise(\n        websocket_url,\n        crate::bridge::bridge_health_check_script(),\n        true,\n    )\n    .await?;\n    Ok(runtime_evaluate_result_is_true(&result))\n}\n\nfn runtime_evaluate_result_is_true(result: &Value) -> bool {\n    result\n        .get(\"result\")\n        .and_then(|result| result.get(\"result\"))\n        .and_then(|result| result.get(\"value\"))\n        .and_then(Value::as_bool)\n        .unwrap_or(false)\n}\n","sourceCodeStart":2461,"sourceCodeEnd":2497,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/1f431ae49b57b3055e0e6845ba6156c6b4232b4d/crates/codex-plus-core/src/launcher.rs#L2461-L2497","documentation":"bridge_health_ok (crates/codex-plus-core/src/launcher.rs:2473) lists DevTools targets, picks the injectable Codex page via cdp::pick_injectable_codex_page_target, and requires the chosen target to carry a webSocketDebuggerUrl before evaluating the health-check script over its WebSocket. The picker's is_injectable_page_target predicate already demands a non-empty webSocketDebuggerUrl, so this guard fires only when that invariant is broken (target list mutated between filter and use, or a forked picker that dropped the predicate) — normally the health check fails earlier with 'No injectable Codex page target found'.","triggerScenarios":"Querying bridge health on a debug port where the selected page target lacks webSocketDebuggerUrl: Chrome-family browsers omit that field for targets being destroyed or for browser-level targets; races where /json/list returns a page that closes before the URL is read; fork code relaxing pick_injectable_codex_page_target.","commonSituations":"Codex app mid-shutdown or mid-navigation when health is polled; calling bridge_health_ok against a plain Chromium debug port (no app:// page with WS URL); stale target caches from a wrapped list_targets.","solutions":["Retry the health check after a short delay — transient target churn resolves once the page settles","Verify the target list manually: GET http://127.0.0.1:<debug_port>/json/list and confirm an entry with type 'page', url app://--/index.html, and a non-empty webSocketDebuggerUrl","If you forked pick_injectable_codex_page_target, restore the is_injectable_page_target filter so only WS-bearing pages are selectable","Ensure you are pointing at Codex's debug port (select_packaged_codex_debug_port), not another Chromium instance"],"exampleFix":"// before: single-shot health probe races target teardown\nlet ok = bridge_health_ok(debug_port).await?; // bail: no websocket URL\n\n// after: tolerate transient churn\nlet ok = match bridge_health_ok(debug_port).await {\n    Ok(v) => v,\n    Err(e) if e.to_string().contains(\"no websocket URL\") => {\n        tokio::time::sleep(Duration::from_millis(500)).await;\n        bridge_health_ok(debug_port).await.unwrap_or(false)\n    }\n    Err(e) => return Err(e),\n};","handlingStrategy":"retry","validationCode":"async fn health_target_has_ws(debug_port: u16) -> bool {\n    match crate::cdp::list_targets(debug_port).await {\n        Ok(targets) => crate::cdp::pick_injectable_codex_page_target(&targets)\n            .map(|t| t.web_socket_debugger_url.as_deref().is_some_and(|u| !u.is_empty()))\n            .unwrap_or(false),\n        Err(_) => false,\n    }\n}","typeGuard":"fn target_has_websocket(t: &crate::cdp::CdpTarget) -> bool {\n    t.web_socket_debugger_url.as_deref().is_some_and(|u| !u.is_empty())\n}","tryCatchPattern":"// Treat as transient churn: one retry, then degrade to 'unhealthy'\nlet healthy = match bridge_health_ok(debug_port).await {\n    Ok(v) => v,\n    Err(e) if e.to_string().contains(\"no websocket URL\") => {\n        tokio::time::sleep(Duration::from_millis(500)).await;\n        bridge_health_ok(debug_port).await.unwrap_or(false)\n    }\n    Err(e) => { tracing::warn!(%e, \"bridge health probe failed\"); false }\n};","preventionTips":["Poll health on an interval rather than once, absorbing target churn","Only point health checks at Codex's own debug port","Keep the webSocketDebuggerUrl filter in pick_injectable_codex_page_target so targets without WS URLs are never selected"],"tags":["rust","cdp","websocket","devtools","health-check","race-condition"],"backgroundTag":"cdp-target-missing-websocket","analyzedSha":"1f431ae49b57b3055e0e6845ba6156c6b4232b4d","analyzedAt":"2026-08-16T20:54:18.598Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}