{"record":{"id":"d2044cfb381b507e","repo":"zeroclaw-labs/zeroclaw","slug":"url-cannot-be-empty-d2044c","errorCode":null,"errorMessage":"URL cannot be empty","messagePattern":"URL cannot be empty","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/http_request.rs","lineNumber":123,"sourceCode":"            nat64_prefixes: domain_guard::parse_nat64_prefixes(\n                &nat64_prefixes,\n                \"security.nat64_prefixes\",\n            )?,\n            config_path: Some(config_path),\n            secrets_encrypt,\n        })\n    }\n\n    #[cfg(test)]\n    fn validate_url(&self, raw_url: &str) -> anyhow::Result<String> {\n        Ok(self.validate_url_policy(raw_url)?.url)\n    }\n\n    fn validate_url_policy(&self, raw_url: &str) -> anyhow::Result<HttpRequestUrlPolicy> {\n        let url = raw_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        if self.allowed_domains.is_empty() {\n            anyhow::bail!(\n                \"HTTP request tool is enabled but no allowed_domains are configured. Add [http_request].allowed_domains in config.toml\"\n            );\n        }\n\n        let host = extract_host(url)?;\n        if let Ok(ip) = host.parse::<IpAddr>() {","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/http_request.rs#L105-L141","documentation":"validate_url_policy is the entry gate for HTTP request targets, reached from validate_url (test surface) and validate_request_target_with_resolver (the live request path). It trims the raw URL and bails with 'URL cannot be empty' when nothing remains. This is the first and cheapest check in a chain that continues with whitespace rejection, http/https scheme enforcement, allowed_domains configuration, and host/IP policy (cloud metadata and link-local blocking). Because the input is trimmed first, a URL consisting solely of spaces, tabs, or newlines also lands here.","triggerScenarios":"Issuing an http_request tool call with {\"url\": \"\"} or {\"url\": \"   \"}; building the URL from an environment variable or config key that is unset (empty string); concatenating a base URL and path where the base is empty; forwarding a user-supplied URL field that was never filled in.","commonSituations":"Config templates ship with an empty endpoint placeholder that is never populated; a secrets/env loader silently substitutes missing values with empty strings; an agent constructs the target from a previous response field that was absent; trailing whitespace from a config file is trimmed into legality only when the whole value was whitespace.","solutions":["Pass a complete, explicit URL including scheme, e.g. {\"url\": \"https://api.example.com/v1/status\"}.","If the URL comes from configuration, validate it at startup and fail fast with a named config error instead of at request time.","Default unset env/config values to a known-good endpoint in the caller rather than forwarding an empty string.","Remember the next checks in the chain: the URL must be http/https, whitespace-free, and its host must be in allowed_domains."],"exampleFix":"// before\nlet args = serde_json::json!({ \"url\": \"\" }); // e.g. env var unset, forwarded as empty\n// tool bails: URL cannot be empty\n\n// after\nlet endpoint = std::env::var(\"API_URL\").unwrap_or_else(|_| \"https://api.example.com\".into());\nlet args = serde_json::json!({ \"url\": format!(\"{endpoint}/v1/status\") });","handlingStrategy":"validation","validationCode":"fn build_http_args(url: &str) -> Option<serde_json::Value> {\n    (!url.trim().is_empty()).then(|| serde_json::json!({ \"url\": url.trim() }))\n}","typeGuard":"fn is_non_empty_http_url(raw: &str) -> bool {\n    let u = raw.trim();\n    !u.is_empty() && (u.starts_with(\"http://\") || u.starts_with(\"https://\"))\n}","tryCatchPattern":"match tool_result {\n    Err(e) if e.to_string().contains(\"URL cannot be empty\") => {\n        // the endpoint variable resolved to blank; fix config resolution, do not retry as-is\n    }\n    other => other,\n}","preventionTips":["Validate required endpoint config at startup (non-empty, http/https, host in allowed_domains) so failures surface at boot, not mid-request.","Avoid silent empty-string fallbacks when env vars or config keys are missing; fail with the missing key's name.","Trim URLs at the boundary; whitespace-only URLs are rejected by this same check after trimming."],"tags":["http","url","validation","security-policy","zeroclaw-tools"],"backgroundTag":"invalid-url","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}