{"record":{"id":"3df97fe3217cf987","repo":"zeroclaw-labs/zeroclaw","slug":"url-host-has-unmatched-ipv6-brackets","errorCode":null,"errorMessage":"URL host has unmatched IPv6 brackets","messagePattern":"URL host has unmatched IPv6 brackets","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/http_request.rs","lineNumber":740,"sourceCode":"            \"http_request: invalid URL\"\n        );\n        anyhow::Error::msg(format!(\"Invalid URL format: {e}\"))\n    })?;\n\n    if !parsed.username().is_empty() || parsed.password().is_some() {\n        anyhow::bail!(\"URL userinfo is not allowed\");\n    }\n\n    let host = parsed\n        .host_str()\n        .ok_or_else(|| anyhow::Error::msg(\"URL must include a host\"))?;\n\n    let trimmed = host.trim();\n    let host_no_brackets = match (trimmed.starts_with('['), trimmed.ends_with(']')) {\n        (true, true) => &trimmed[1..trimmed.len() - 1],\n        (false, false) => trimmed,\n        _ => {\n            anyhow::bail!(\"URL host has unmatched IPv6 brackets\");\n        }\n    };\n    let host = host_no_brackets.trim_end_matches('.').to_lowercase();\n\n    if host.is_empty() {\n        anyhow::bail!(\"URL must include a valid host\");\n    }\n\n    Ok(host)\n}\n\nfn extract_port(url: &str) -> anyhow::Result<u16> {\n    let parsed = reqwest::Url::parse(url)\n        .map_err(|e| anyhow::Error::msg(format!(\"Invalid URL format: {e}\")))?;\n\n    parsed\n        .port_or_known_default()\n        .ok_or_else(|| anyhow::Error::msg(\"URL must include a valid port\"))","sourceCodeStart":722,"sourceCodeEnd":758,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/http_request.rs#L722-L758","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nlet url = format!(\"http://[{}:{port}\", ipv6); // missing ']'\n\n// after\nlet url = format!(\"http://[{}]:{port}\", ipv6);","handlingStrategy":"validation","validationCode":"fn ipv6_host_brackets_matched(host: &str) -> bool {\n    let t = host.trim();\n    t.starts_with('[') == t.ends_with(']')\n}","typeGuard":"fn is_well_formed_ipv6_url(url: &str) -> bool {\n    reqwest::Url::parse(url).map(|u| {\n        u.host_str().map(|h| h.starts_with('[') == h.ends_with(']')).unwrap_or(true)\n    }).unwrap_or(false)\n}","tryCatchPattern":"let result = tool.execute(args).await?;\nif let Some(err) = &result.error {\n    if err.contains(\"unmatched IPv6 brackets\") {\n        // rebuild the URL as http://[<ipv6>]:<port>/ and retry\n    }\n}","preventionTips":["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."],"tags":["http","url","ipv6","validation","zeroclaw"],"backgroundTag":"malformed-ipv6-url","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}