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

URL host has unmatched IPv6 brackets

Error message

URL host has unmatched IPv6 brackets

What it means

Thrown by extract_host (crates/zeroclaw-tools/src/http_request.rs:740) when the extracted host string has exactly one IPv6 bracket — starts with '[' but does not end with ']', or vice versa. RFC 3986 requires IPv6 literals in URLs to be fully wrapped in matched brackets (http://[2001:db8::1]:8080/); the bracket-normalization step rejects half-wrapped hosts because they cannot be safely classified as an IP literal for the SSRF checks.

Source

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

            "http_request: invalid URL"
        );
        anyhow::Error::msg(format!("Invalid URL format: {e}"))
    })?;

    if !parsed.username().is_empty() || parsed.password().is_some() {
        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"))

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Wrap the IPv6 literal in matched brackets: "http://[2001:db8::1]:443/path".
  2. When constructing programmatically, use a URL builder or format!("http://[{ip}]:{port}") so brackets are always paired.
  3. Use hostnames instead of raw IPv6 literals where possible.

Example fix

// before
let url = format!("http://[{}:{port}", ipv6); // missing ']'

// after
let url = format!("http://[{}]:{port}", ipv6);
Defensive patterns

Strategy: validation

Validate before calling

fn ipv6_host_brackets_matched(host: &str) -> bool {
    let t = host.trim();
    t.starts_with('[') == t.ends_with(']')
}

Type guard

fn is_well_formed_ipv6_url(url: &str) -> bool {
    reqwest::Url::parse(url).map(|u| {
        u.host_str().map(|h| h.starts_with('[') == h.ends_with(']')).unwrap_or(true)
    }).unwrap_or(false)
}

Try / catch

let result = tool.execute(args).await?;
if let Some(err) = &result.error {
    if err.contains("unmatched IPv6 brackets") {
        // rebuild the URL as http://[<ipv6>]:<port>/ and retry
    }
}

Prevention

When it happens

Trigger: url = "http://[::1:8080/" (missing ']'); "http://2001:db8::1]/" (stray closing bracket); URLs assembled by concatenating '[' + ipv6 + port without re-adding the closing bracket; hand-typed IPv6 URLs in configs or test fixtures where one bracket was dropped.

Common situations: String-building IPv6 URLs by hand or with format! that forgets the closing bracket; editors auto-deleting one bracket; migrating from tools that accepted unbracketed IPv6 to this stricter parser.

Related errors


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