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

  1. 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.
  2. 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.
  3. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/4f7d4438aeea52eb. Report an issue: GitHub.