zeroclaw-labs/zeroclaw · error · anyhow::Error

URL must include a valid host

Error message

URL must include a valid host

What it means

Thrown by extract_host in the http_request tool after the URL parsed and passed the scheme and userinfo checks, but its host reduced to an empty string once trailing root-label dots were stripped and the value lowercased. In other words the host consisted solely of one or more '.' characters (for example 'https://.'). A dot-only host cannot be resolved or SSRF-validated, so the request is rejected before any network I/O happens.

Source

Thrown at crates/zeroclaw-tools/src/http_request.rs:746

        anyhow::bail!("URL userinfo is not allowed");
    }

    let host = parsed
        .host_str()
        .ok_or_else(|| anyhow::Error::msg("URL must include a host"))?;

    let trimmed = host.trim();
    let host_no_brackets = match (trimmed.starts_with('['), trimmed.ends_with(']')) {
        (true, true) => &trimmed[1..trimmed.len() - 1],
        (false, false) => trimmed,
        _ => {
            anyhow::bail!("URL host has unmatched IPv6 brackets");
        }
    };
    let host = host_no_brackets.trim_end_matches('.').to_lowercase();

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

    Ok(host)
}

fn extract_port(url: &str) -> anyhow::Result<u16> {
    let parsed = reqwest::Url::parse(url)
        .map_err(|e| anyhow::Error::msg(format!("Invalid URL format: {e}")))?;

    parsed
        .port_or_known_default()
        .ok_or_else(|| anyhow::Error::msg("URL must include a valid port"))
}

async fn resolve_host_for_request(host: String, port: u16) -> anyhow::Result<Vec<SocketAddr>> {
    let addrs = tokio::net::lookup_host((host.as_str(), port))
        .await
        .map_err(|e| anyhow::Error::msg(format!("Failed to resolve host '{host}': {e}")))?

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Replace the dot-only host with a real hostname, e.g. 'https://api.example.com/path'.
  2. If the URL is built dynamically, print it before invoking the tool and fix the interpolation that left a placeholder in the host position.
  3. Pre-validate hosts (non-empty after trimming trailing dots) before passing URLs to the tool.

Example fix

// before
let url = format!("https://{}", host_placeholder); // host_placeholder = "."
tool_http_request(url).await?; // -> URL must include a valid host

// after
let url = format!("https://api.example.com{}", path);
tool_http_request(url).await?;
Defensive patterns

Strategy: validation

Validate before calling

fn has_valid_http_host(url: &str) -> bool {
    let Ok(parsed) = reqwest::Url::parse(url) else { return false };
    match parsed.host_str() {
        Some(h) => !h.trim().trim_end_matches('.').is_empty(),
        None => false,
    }
}

Type guard

fn is_http_request_url(url: &str) -> bool {
    (url.starts_with("http://") || url.starts_with("https://")) && has_valid_http_host(url)
}

Try / catch

if let Err(e) = run_http_request(url).await {
    if e.to_string().contains("URL must include a valid host") {
        // treat as caller input error: surface a fixable message, do not retry
    }
}

Prevention

When it happens

Trigger: Calling the http_request tool with a URL whose host part is only dots: 'http://.', 'https://..', 'https://...'. Any input where reqwest::Url::parse succeeds and host_str() returns a non-empty string made entirely of '.' characters hits this bail at http_request.rs:746. It cannot be reached with a truly empty host (that is the earlier 'URL must include a host' error at line 733) or with unmatched IPv6 brackets (line 740).

Common situations: Placeholder URLs pasted from templates or documentation ('http://...'), typos where dots were typed instead of a hostname, dynamic construction like format!("https://{}", placeholder) where the placeholder was never filled in, and test fixtures that used dot-only hosts.

Related errors


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