{"record":{"id":"b1e3aa582a63e79f","repo":"zeroclaw-labs/zeroclaw","slug":"url-has-no-host","errorCode":null,"errorMessage":"URL has no host: {}","messagePattern":"URL has no host: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/browser_delegate.rs","lineNumber":96,"sourceCode":"                    .with_attrs(::serde_json::json!({\n                        \"url\": url,\n                        \"error\": format!(\"{}\", e),\n                    })),\n                \"browser_delegate: invalid URL\"\n            );\n            anyhow::Error::msg(format!(\"invalid URL '{}': {}\", url, e))\n        })?;\n\n        // Only allow http/https schemes\n        let scheme = parsed.scheme();\n        if scheme != \"http\" && scheme != \"https\" {\n            anyhow::bail!(\"unsupported URL scheme: {}\", scheme);\n        }\n\n        let domain = parsed.host_str().unwrap_or(\"\").to_string();\n\n        if domain.is_empty() {\n            anyhow::bail!(\"URL has no host: {}\", url);\n        }\n\n        // Check blocked domains first (deny takes precedence)\n        for blocked in &self.config.blocked_domains {\n            if domain_matches(&domain, blocked) {\n                anyhow::bail!(\"domain '{}' is blocked by browser_delegate policy\", domain);\n            }\n        }\n\n        // If allowed_domains is non-empty, it acts as an allowlist\n        if !self.config.allowed_domains.is_empty() {\n            let allowed = self\n                .config\n                .allowed_domains\n                .iter()\n                .any(|d| domain_matches(&domain, d));\n            if !allowed {\n                anyhow::bail!(","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/browser_delegate.rs#L78-L114","documentation":"After parsing, browser_delegate requires a non-empty host component from the parsed URL. URLs that parse but carry no host — 'http:///path', 'https://?q=1' — fail here. The host is required because the domain allow/block checks that follow operate on it.","triggerScenarios":"A task URL like \"http:///health\" (triple slash), \"https://?next=1\", or a malformed paste where the domain was dropped.","commonSituations":"String-concatenation bugs that lose the host between scheme and path; copy-paste truncation; model-generated URLs that omit the domain.","solutions":["Fix the URL to include an explicit host: https://example.com/health","Assemble URLs with a builder (reqwest::Url, url::Url::join) rather than string concatenation","Pre-check candidate URLs with url.parse::<Url>() and host_str().is_some() before submitting the task"],"exampleFix":"// before\n\"http:///health\"\n// after\n\"https://example.com/health\"","handlingStrategy":"validation","validationCode":"let parsed: reqwest::Url = candidate.parse()?;\nif parsed.host_str().map(str::is_empty).unwrap_or(true) {\n    return Err(format!(\"URL has no host: {candidate}\"));\n}","typeGuard":null,"tryCatchPattern":"match delegate.execute(args).await {\n    Ok(res) if res.success => { /* ... */ }\n    Ok(res) => {\n        if res.error.as_deref().unwrap_or_default().contains(\"no host\") {\n            // fix URL construction; host is missing\n        }\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Build URLs with a URL library, never by string concatenation","Validate model-generated URLs with url.parse() plus host_str() before use"],"tags":["browser-delegate","url","host","validation"],"backgroundTag":"url-missing-host","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}