{"record":{"id":"3b8d08a7b9e575c3","repo":"zeroclaw-labs/zeroclaw","slug":"url-must-include-a-valid-host","errorCode":null,"errorMessage":"URL must include a valid host","messagePattern":"URL must include a valid host","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/browser_open.rs","lineNumber":321,"sourceCode":"\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)]\nmod tests {\n    use super::*;\n    use zeroclaw_config::autonomy::AutonomyLevel;\n    use zeroclaw_config::policy::SecurityPolicy;\n\n    fn test_tool(allowed_domains: Vec<&str>) -> BrowserOpenTool {\n        let security = Arc::new(SecurityPolicy {\n            autonomy: AutonomyLevel::Supervised,\n            ..SecurityPolicy::default()\n        });\n        BrowserOpenTool::new(\n            security,","sourceCodeStart":303,"sourceCodeEnd":339,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/browser_open.rs#L303-L339","documentation":"After the earlier checks, extract_host splits the authority on ':', trims whitespace, strips trailing dots, and lowercases to obtain the host (browser_open.rs:312-318). If what remains is empty, the authority contained only a port, dots, or separators and no actual hostname, so this bail fires (browser_open.rs:320-322). It catches host-missing cases that slip past the empty-authority check, such as 'https://:8080'.","triggerScenarios":"Passing 'https://:8080/x' (port-only authority), 'https://.' (dot-only authority), or URLs where the host segment between scheme and ':' is blank. Almost always a URL-building bug that dropped the hostname but kept a port or punctuation.","commonSituations":"format! templates that interpolate an Option<&str> host as empty while a static ':port' suffix remains; configs where a host key is typo'd so lookup yields None-rendered-as-empty; hand-edited URLs missing the name.","solutions":["Include a real hostname: 'https://service.internal:8080/x' instead of 'https://:8080/x'.","Fix the URL construction so the host variable is resolved and non-empty before formatting.","Add a startup assertion in your own code that the built URL has a non-blank host to catch regressions early."],"exampleFix":"// before\nlet url = format!(\"https://:{}\", port); // -> \"https://:8080\"\n\n// after\nlet url = format!(\"https://{host}:{port}\", host = \"service.internal\");","handlingStrategy":"validation","validationCode":"fn url_host_is_substantive(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    if authority.is_empty() || authority.contains('@') || authority.starts_with('[') {\n        return false;\n    }\n    let host = authority.split(':').next().unwrap_or(\"\");\n    !host.trim().trim_end_matches('.').is_empty()\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Assert the host component resolves to a non-empty string before formatting URLs with ':port'.","Watch for Option-rendered-as-empty host variables in format! calls.","Run a URL lint (parse + host check) over config-generated URLs in tests."],"tags":["url-validation","host","browser","rust","zeroclaw"],"backgroundTag":"missing-url-host","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}