{"record":{"id":"75bb7a15057b2aca","repo":"zeroclaw-labs/zeroclaw","slug":"url-cannot-be-empty-75bb7a","errorCode":null,"errorMessage":"URL cannot be empty","messagePattern":"URL cannot be empty","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"crates/zeroclaw-tools/src/text_browser.rs","lineNumber":218,"sourceCode":"        })\n    }\n\n    /// Build the command arguments for the selected browser with `-dump` flag.\n    fn build_dump_args(_browser: &str, url: &str) -> Vec<String> {\n        // All supported browsers (lynx, links, w3m) use the same `-dump` flag\n        vec![\"-dump\".to_string(), url.to_string()]\n    }\n}\n\nfn validate_text_browser_url(\n    url: &str,\n    allowed_private_hosts: &[String],\n    validate_dns: impl FnOnce(&str, bool) -> anyhow::Result<()>,\n) -> anyhow::Result<String> {\n    let url = url.trim();\n\n    if url.is_empty() {\n        anyhow::bail!(\"URL cannot be empty\");\n    }\n\n    if url.chars().any(char::is_whitespace) {\n        anyhow::bail!(\"URL cannot contain whitespace\");\n    }\n\n    if !url.starts_with(\"http://\") && !url.starts_with(\"https://\") {\n        anyhow::bail!(\"Only http:// and https:// URLs are allowed\");\n    }\n\n    let parsed = reqwest::Url::parse(url)\n        .map_err(|e| anyhow::Error::msg(format!(\"Invalid URL format: {e}\")))?;\n\n    if !parsed.username().is_empty() || parsed.password().is_some() {\n        anyhow::bail!(\"URL userinfo is not allowed\");\n    }\n\n    let host_str = parsed","sourceCodeStart":200,"sourceCodeEnd":236,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/text_browser.rs#L200-L236","documentation":"All text_browser URL validation paths (validate_url, validate_url_with_dns_check, validate_url_for_execute) funnel into validate_text_browser_url, whose first check after trimming is that the URL is non-empty. An empty or whitespace-only url parameter bails immediately with this message.","triggerScenarios":"Calling the tool with url \"\" or \"   \"; passing a variable that was never populated (empty string from a failed upstream lookup); template substitution that produced nothing.","commonSituations":"Pipeline stages where the URL comes from a previous step (RSS parse, config lookup) that silently returned an empty string; defaults declared as \"\" instead of Option.","solutions":["Pass a concrete http(s) URL","Check upstream: if the URL source can be empty, make it an Option and skip the call instead of forwarding \"\"","Add a preflight trim+is_empty assertion in the caller"],"exampleFix":"// before\nlet url = config.get(\"url\").unwrap_or_default();\ntool.execute(json!({\"url\": url, \"browser\": \"lynx\"})).await?;\n// after\nlet Some(url) = config.get(\"url\") else { return Ok(()) };\nif url.trim().is_empty() { return Ok(()) }\ntool.execute(json!({\"url\": url.trim(), \"browser\": \"lynx\"})).await?;","handlingStrategy":"validation","validationCode":"let url = raw.trim();\nif url.is_empty() { /* skip or prompt; never call the tool */ }","typeGuard":"fn is_nonempty_url(u: &str) -> bool { !u.trim().is_empty() }","tryCatchPattern":"Err(e) if e.to_string() == \"URL cannot be empty\" => {\n    // upstream produced no URL: log and skip this item rather than retry\n}","preventionTips":["Model optional URLs as Option<String>, not String::default()","Assert non-empty inputs at the boundary where they enter your pipeline","Fail fast on empty template/config values in tests"],"tags":["url","validation","input"],"backgroundTag":"url-validation-failed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}