{"record":{"id":"2649d8409324748d","repo":"zeroclaw-labs/zeroclaw","slug":"failed-to-open-url-with-default-browser-launchers","errorCode":null,"errorMessage":"Failed to open URL with default browser launchers; Brave compatibility fallback also failed. Last error: {last_error}","messagePattern":"Failed to open URL with default browser launchers; Brave compatibility fallback also failed\\. Last error: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/browser_open.rs","lineNumber":234,"sourceCode":"            \"gio\",\n            \"sensible-browser\",\n            \"brave-browser\",\n            \"brave\",\n        ] {\n            let mut command = tokio::process::Command::new(cmd);\n            if cmd == \"gio\" {\n                command.arg(\"open\");\n            }\n            command.arg(url);\n            let label = if cmd == \"gio\" { \"gio open\" } else { cmd };\n            match run_browser_launcher(command, label).await {\n                Ok(()) => return Ok(()),\n                Err(error) => last_error = error,\n            }\n        }\n\n        // TODO(compat): remove Brave fallback commands (brave-browser/brave) once default launcher coverage is validated.\n        anyhow::bail!(\n            \"Failed to open URL with default browser launchers; Brave compatibility fallback also failed. Last error: {last_error}\"\n        );\n    }\n\n    #[cfg(target_os = \"windows\")]\n    {\n        // Use direct process invocation (not `cmd /C start`) to avoid shell\n        // metacharacter interpretation in URLs (e.g. `&` in query strings).\n        let mut command = tokio::process::Command::new(\"rundll32\");\n        command.arg(\"url.dll,FileProtocolHandler\").arg(url);\n        let primary_error =\n            match run_browser_launcher(command, \"rundll32 default-browser launcher\").await {\n                Ok(()) => return Ok(()),\n                Err(error) => error,\n            };\n\n        // TODO(compat): remove Brave fallback after default-browser launch has been stable across Windows environments.\n        let mut brave_error = String::new();","sourceCodeStart":216,"sourceCodeEnd":252,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/browser_open.rs#L216-L252","documentation":"On Linux, BrowserOpenTool tries five launchers in order (xdg-open, gio open, sensible-browser, brave-browser, brave) via run_browser_launcher, each under a 10-second timeout (BROWSER_OPEN_LAUNCH_TIMEOUT, browser_open.rs:8). This error means every launcher failed; last_error comes from the final attempt (brave) and is one of '<label> exited with status N', '<label> not runnable: <spawn error>', or '<label> timed out after 10s' (browser_open.rs:177-182). Because the five attempts run sequentially, a fully broken host can take up to ~50 seconds before this bail fires.","triggerScenarios":"Calling the browser_open tool with an already-validated http(s) URL on Linux where: (1) none of the five launcher binaries are on PATH (spawn returns NotFound -> 'not runnable'), (2) xdg-open/gio exit non-zero because no desktop environment or default browser is registered, or (3) a launcher hangs (e.g. gio waiting on a stuck desktop portal) and hits the 10s timeout.","commonSituations":"Headless servers, Docker containers, WSL without GUI, CI runners, and minimal distros that lack the xdg-utils package or have no default web browser registered. Also seen after DE reinstalls where xdg-open exists but its browser associations are broken.","solutions":["Install xdg-utils (Debian/Ubuntu: apt-get install -y xdg-utils; Alpine: apk add xdg-utils) so xdg-open exists and succeeds first.","Register a default browser on desktop systems: xdg-settings set default-webbrowser <browser.desktop> or fix gio/gnome associations.","On headless hosts, stop routing browser_open there: disable the browser tool in config instead of retrying.","Read the embedded last_error to classify the failure: 'not runnable' = missing binary (install it), 'timed out' = hung launcher (fix DE/portal), 'exited with status N' = launcher present but refused (check DISPLAY/XDG portal)."],"exampleFix":"# before: headless container, every launcher missing -> error\n# (Dockerfile with no xdg-utils)\n\n# after: provide the primary launcher in the image\n# Dockerfile\nRUN apt-get update && apt-get install -y --no-install-recommends xdg-utils","handlingStrategy":"fallback","validationCode":"use std::path::Path;\n\nfn has_linux_browser_launcher() -> bool {\n    let Ok(paths) = std::env::var(\"PATH\") else { return false };\n    [\"xdg-open\", \"gio\", \"sensible-browser\"].iter().any(|cmd| {\n        paths.split(':').any(|dir| !dir.is_empty() && Path::new(dir).join(cmd).exists())\n    })\n}\n\n// before registering the tool:\n// if !has_linux_browser_launcher() { skip BrowserOpenTool or log a warning }","typeGuard":null,"tryCatchPattern":"match browser_tool.execute(json!({\"url\": url.clone()})).await {\n    Ok(result) if result.success => { /* opened */ }\n    Ok(result) => {\n        let detail = result.error.as_deref().unwrap_or_default();\n        eprintln!(\"browser_open failed ({detail}); open manually: {url}\");\n    }\n    Err(e) => eprintln!(\"tool error: {e}\"),\n}","preventionTips":["Install xdg-utils in every Docker image / server baseline that will run the browser tool.","Probe for a launcher on PATH at startup and skip registering BrowserOpenTool when none exists.","Register a default browser (xdg-settings) on desktop deployments so xdg-open exits 0.","Remember the 10s per-launcher timeout: on broken hosts the tool can block for up to ~50s before failing."],"tags":["linux","browser","headless","xdg-open","rust","zeroclaw"],"backgroundTag":"no-default-browser-found","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}