zeroclaw-labs/zeroclaw · error · anyhow::Error
URL userinfo is not allowed
Error message
URL userinfo is not allowed
What it means
Thrown by extract_host (crates/zeroclaw-tools/src/http_request.rs:728) when the parsed URL carries userinfo — a username and/or password before the host, as in "https://user:pass@example.com". Credentials embedded in URLs leak into logs, referers, and error messages, and defeat the Authorization/auth_secret secret handling, so the tool rejects them outright.
Source
Thrown at crates/zeroclaw-tools/src/http_request.rs:728
.with_attrs(::serde_json::json!({"url": url})),
"http_request: non-http(s) URL rejected"
);
anyhow::bail!("Only http:// and https:// URLs are allowed");
}
let parsed = reqwest::Url::parse(url).map_err(|e| {
::zeroclaw_log::record!(
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
.with_outcome(::zeroclaw_log::EventOutcome::Failure)
.with_attrs(::serde_json::json!({"url": url})),
"http_request: invalid URL"
);
anyhow::Error::msg(format!("Invalid URL format: {e}"))
})?;
if !parsed.username().is_empty() || parsed.password().is_some() {
anyhow::bail!("URL userinfo is not allowed");
}
let host = parsed
.host_str()
.ok_or_else(|| anyhow::Error::msg("URL must include a host"))?;
let trimmed = host.trim();
let host_no_brackets = match (trimmed.starts_with('['), trimmed.ends_with(']')) {
(true, true) => &trimmed[1..trimmed.len() - 1],
(false, false) => trimmed,
_ => {
anyhow::bail!("URL host has unmatched IPv6 brackets");
}
};
let host = host_no_brackets.trim_end_matches('.').to_lowercase();
if host.is_empty() {
anyhow::bail!("URL must include a valid host");View on GitHub (pinned to 88bb9c8533)
Solutions
- Remove userinfo from the URL and send credentials via the Authorization header, ideally through the auth_secret parameter so the value stays in config.toml.
- For basic auth, store the Base64 of "user:pass" (or the raw credentials, depending on your scheme) as a secret and pass its name in auth_secret.
- Rotate any credential that was embedded in a logged URL.
Example fix
# before
url = "https://admin:s3cret@example.com/api"
# after
[http_request.secrets]
basic_auth = "Basic YWRtaW46czNjcmV0" # base64 of admin:s3cret
# caller: {"url": "https://example.com/api", "auth_secret": "basic_auth"} Defensive patterns
Strategy: validation
Validate before calling
fn url_has_userinfo(url: &str) -> bool {
reqwest::Url::parse(url)
.map(|u| !u.username().is_empty() || u.password().is_some())
.unwrap_or(false)
} Try / catch
let result = tool.execute(args).await?;
if let Some(err) = &result.error {
if err.contains("URL userinfo is not allowed") {
// move credentials to auth_secret / Authorization header, then retry
}
} Prevention
- Never construct URLs with embedded user:pass; keep credentials in [http_request.secrets].
- Sanitize pasted URLs by stripping anything before the host.
- If a credential ever landed in a URL, rotate it: URLs end up in logs.
When it happens
Trigger: url = "https://admin:secret@192.168.1.5/api"; pasting a database-style or basic-auth URL from browser history/docs (browsers show user:pass@host); scrapers reusing URLs that embed API keys as userinfo; LLM-generated URLs copying an authenticated link verbatim.
Common situations: Basic-auth protected endpoints where users copy the authenticated URL instead of the host; legacy bookmarks containing credentials; API docs that illustrate auth via userinfo; secrets accidentally committed inside URLs.
Related errors
- URL cannot contain whitespace
- Only http:// and https:// URLs are allowed
- Blocked cloud metadata host: {host}
- Blocked link-local host: {host}; 169.254.0.0/16 is blocked u
- Blocked local/private host: {host}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/4f7d4438aeea52eb.
Report an issue: GitHub.