zeroclaw-labs/zeroclaw · error

URL must include a valid host

Error message

URL must include a valid host

What it means

After the earlier checks, extract_host splits the authority on ':', trims whitespace, strips trailing dots, and lowercases to obtain the host (browser_open.rs:312-318). If what remains is empty, the authority contained only a port, dots, or separators and no actual hostname, so this bail fires (browser_open.rs:320-322). It catches host-missing cases that slip past the empty-authority check, such as 'https://:8080'.

Source

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

    if authority.contains('@') {
        anyhow::bail!("URL userinfo is not allowed");
    }

    if authority.starts_with('[') {
        anyhow::bail!("IPv6 hosts are not supported in browser_open");
    }

    let host = authority
        .split(':')
        .next()
        .unwrap_or_default()
        .trim()
        .trim_end_matches('.')
        .to_lowercase();

    if host.is_empty() {
        anyhow::bail!("URL must include a valid host");
    }

    Ok(host)
}

#[cfg(test)]
mod tests {
    use super::*;
    use zeroclaw_config::autonomy::AutonomyLevel;
    use zeroclaw_config::policy::SecurityPolicy;

    fn test_tool(allowed_domains: Vec<&str>) -> BrowserOpenTool {
        let security = Arc::new(SecurityPolicy {
            autonomy: AutonomyLevel::Supervised,
            ..SecurityPolicy::default()
        });
        BrowserOpenTool::new(
            security,

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Include a real hostname: 'https://service.internal:8080/x' instead of 'https://:8080/x'.
  2. Fix the URL construction so the host variable is resolved and non-empty before formatting.
  3. Add a startup assertion in your own code that the built URL has a non-blank host to catch regressions early.

Example fix

// before
let url = format!("https://:{}", port); // -> "https://:8080"

// after
let url = format!("https://{host}:{port}", host = "service.internal");
Defensive patterns

Strategy: validation

Validate before calling

fn url_host_is_substantive(url: &str) -> bool {
    let Some(rest) = url
        .strip_prefix("https://")
        .or_else(|| url.strip_prefix("http://"))
    else {
        return false;
    };
    let authority = rest.split(['/', '?', '#']).next().unwrap_or("");
    if authority.is_empty() || authority.contains('@') || authority.starts_with('[') {
        return false;
    }
    let host = authority.split(':').next().unwrap_or("");
    !host.trim().trim_end_matches('.').is_empty()
}

Prevention

When it happens

Trigger: Passing 'https://:8080/x' (port-only authority), 'https://.' (dot-only authority), or URLs where the host segment between scheme and ':' is blank. Almost always a URL-building bug that dropped the hostname but kept a port or punctuation.

Common situations: format! templates that interpolate an Option<&str> host as empty while a static ':port' suffix remains; configs where a host key is typo'd so lookup yields None-rendered-as-empty; hand-edited URLs missing the name.

Related errors


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