zeroclaw-labs/zeroclaw · error

Failed to open URL with default browser launchers; Brave com

Error message

Failed to open URL with default browser launchers; Brave compatibility fallback also failed. Last error: {last_error}

What it means

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.

Source

Thrown at crates/zeroclaw-tools/src/browser_open.rs:234

            "gio",
            "sensible-browser",
            "brave-browser",
            "brave",
        ] {
            let mut command = tokio::process::Command::new(cmd);
            if cmd == "gio" {
                command.arg("open");
            }
            command.arg(url);
            let label = if cmd == "gio" { "gio open" } else { cmd };
            match run_browser_launcher(command, label).await {
                Ok(()) => return Ok(()),
                Err(error) => last_error = error,
            }
        }

        // TODO(compat): remove Brave fallback commands (brave-browser/brave) once default launcher coverage is validated.
        anyhow::bail!(
            "Failed to open URL with default browser launchers; Brave compatibility fallback also failed. Last error: {last_error}"
        );
    }

    #[cfg(target_os = "windows")]
    {
        // Use direct process invocation (not `cmd /C start`) to avoid shell
        // metacharacter interpretation in URLs (e.g. `&` in query strings).
        let mut command = tokio::process::Command::new("rundll32");
        command.arg("url.dll,FileProtocolHandler").arg(url);
        let primary_error =
            match run_browser_launcher(command, "rundll32 default-browser launcher").await {
                Ok(()) => return Ok(()),
                Err(error) => error,
            };

        // TODO(compat): remove Brave fallback after default-browser launch has been stable across Windows environments.
        let mut brave_error = String::new();

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Install xdg-utils (Debian/Ubuntu: apt-get install -y xdg-utils; Alpine: apk add xdg-utils) so xdg-open exists and succeeds first.
  2. Register a default browser on desktop systems: xdg-settings set default-webbrowser <browser.desktop> or fix gio/gnome associations.
  3. On headless hosts, stop routing browser_open there: disable the browser tool in config instead of retrying.
  4. 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).

Example fix

# before: headless container, every launcher missing -> error
# (Dockerfile with no xdg-utils)

# after: provide the primary launcher in the image
# Dockerfile
RUN apt-get update && apt-get install -y --no-install-recommends xdg-utils
Defensive patterns

Strategy: fallback

Validate before calling

use std::path::Path;

fn has_linux_browser_launcher() -> bool {
    let Ok(paths) = std::env::var("PATH") else { return false };
    ["xdg-open", "gio", "sensible-browser"].iter().any(|cmd| {
        paths.split(':').any(|dir| !dir.is_empty() && Path::new(dir).join(cmd).exists())
    })
}

// before registering the tool:
// if !has_linux_browser_launcher() { skip BrowserOpenTool or log a warning }

Try / catch

match browser_tool.execute(json!({"url": url.clone()})).await {
    Ok(result) if result.success => { /* opened */ }
    Ok(result) => {
        let detail = result.error.as_deref().unwrap_or_default();
        eprintln!("browser_open failed ({detail}); open manually: {url}");
    }
    Err(e) => eprintln!("tool error: {e}"),
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/2649d8409324748d. Report an issue: GitHub.