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
- Wrap the IPv6 literal in matched brackets: "http://[2001:db8::1]:443/path".
- When constructing programmatically, use a URL builder or format!("http://[{ip}]:{port}") so brackets are always paired.
- 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
- Always format IPv6 URLs with matched brackets: http://[2001:db8::1]:port/.
- Use format!("https://[{addr}]:{port}") instead of hand-assembling bracket fragments.
- Prefer hostnames over raw IPv6 literals in configs and fixtures.
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
- URL cannot contain whitespace
- Only http:// and https:// URLs are allowed
- providers.models.{profile_name}.uri must use http/https
- URL cannot be empty
- Host '{host}' is not in http_request.allowed_domains
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/3df97fe3217cf987.
Report an issue: GitHub.