{"record":{"id":"18fecb9dfed0bc1b","repo":"zeroclaw-labs/zeroclaw","slug":"ipv6-hosts-are-not-supported-in-browser-open","errorCode":null,"errorMessage":"IPv6 hosts are not supported in browser_open","messagePattern":"IPv6 hosts are not supported in browser_open","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/browser_open.rs","lineNumber":309,"sourceCode":"            WARN,\n            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)\n                .with_outcome(::zeroclaw_log::EventOutcome::Failure)\n                .with_attrs(::serde_json::json!({\"url\": url})),\n            \"browser_open: invalid URL\"\n        );\n        anyhow::Error::msg(\"Invalid URL\")\n    })?;\n\n    if authority.is_empty() {\n        anyhow::bail!(\"URL must include a host\");\n    }\n\n    if authority.contains('@') {\n        anyhow::bail!(\"URL userinfo is not allowed\");\n    }\n\n    if authority.starts_with('[') {\n        anyhow::bail!(\"IPv6 hosts are not supported in browser_open\");\n    }\n\n    let host = authority\n        .split(':')\n        .next()\n        .unwrap_or_default()\n        .trim()\n        .trim_end_matches('.')\n        .to_lowercase();\n\n    if host.is_empty() {\n        anyhow::bail!(\"URL must include a valid host\");\n    }\n\n    Ok(host)\n}\n\n#[cfg(test)]","sourceCodeStart":291,"sourceCodeEnd":327,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/browser_open.rs#L291-L327","documentation":"extract_host rejects authorities beginning with '[', which is the RFC 3986 marker for an IPv6 literal such as [2001:db8::1] or [::1] (browser_open.rs:308-310). The colon-based host parser downstream cannot handle bracketed v6 addresses, so browser_open explicitly refuses them instead of mis-parsing. It is a capability limit, not a transient fault.","triggerScenarios":"Calling browser_open with 'http://[::1]:8080/health', 'https://[2001:db8::25de::ad24]/page', or any URL whose host is written as a bracketed IPv6 literal. Typical when targeting local or LAN services addressed by raw v6 address.","commonSituations":"Local development against services bound to ::1, dual-stack networks where DNS returns v6 and someone copied the literal, and monitoring links to v6-only hosts.","solutions":["Use a hostname instead of the literal: map a DNS name or /etc/hosts entry (e.g. 'service.local') to the IPv6 address and open 'http://service.local:8080/health'.","Use the IPv4 form of the address if the service also listens on v4; note that private/local v4 hosts additionally require a browser.allowed_private_hosts entry.","Add the chosen hostname to [browser].allowed_domains in config.toml so the allowlist check passes."],"exampleFix":"// before\n{\"url\": \"http://[::1]:8080/health\"}\n\n// after: /etc/hosts maps service.local -> ::1, config allowlists it\n{\"url\": \"http://service.local:8080/health\"}\n# config.toml\n[browser]\nallowed_domains = [\"service.local\"]","handlingStrategy":"validation","validationCode":"fn url_host_is_not_ipv6_literal(url: &str) -> bool {\n    let Some(rest) = url\n        .strip_prefix(\"https://\")\n        .or_else(|| url.strip_prefix(\"http://\"))\n    else {\n        return false;\n    };\n    let authority = rest.split(['/', '?', '#']).next().unwrap_or(\"\");\n    !authority.starts_with('[')\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Use DNS names or /etc/hosts entries for IPv6 services instead of bracketed literals.","Add the hostname you choose to [browser].allowed_domains (or allowed_private_hosts for local ones).","Reject URLs whose authority starts with '[' before calling browser_open to get a clearer upstream error."],"tags":["url-validation","ipv6","browser","rust","zeroclaw"],"backgroundTag":"ipv6-host-not-supported","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}