zeroclaw-labs/zeroclaw · error · anyhow::Error

login failed ({status}): {body}

Error message

login failed ({status}): {body}

What it means

When `bot_token` is unset, the channel performs the password flow `POST /api/v4/users/login` and expects a session token in the `Token` response header. This error is raised when the login response has a non-2xx status; the status and body are included. 401 means wrong login_id/password; other statuses cover locked accounts, SSO-only servers, or rate limiting.

Source

Thrown at crates/zeroclaw-channels/src/mattermost.rs:318

    /// Perform the Mattermost password login flow and return the session
    /// token. The session token is returned via the `Token` response header
    /// per Mattermost API v4.
    async fn login(&self, login_id: &str, password: &str) -> Result<String> {
        let resp = self
            .http_client()
            .post(format!("{}/api/v4/users/login", self.base_url))
            .json(&serde_json::json!({
                "login_id": login_id,
                "password": password,
            }))
            .send()
            .await
            .context("login request failed")?;
        if !resp.status().is_success() {
            let status = resp.status();
            let body = resp.text().await.unwrap_or_default();
            bail!("login failed ({status}): {body}");
        }
        let token = resp
            .headers()
            .get("Token")
            .and_then(|v| v.to_str().ok())
            .ok_or_else(|| {
                ::zeroclaw_log::record!(
                    ERROR,
                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
                        .with_outcome(::zeroclaw_log::EventOutcome::Failure),
                    "login succeeded but the response had no Token header"
                );
                anyhow::Error::msg("login succeeded but the response had no Token header")
            })?
            .to_string();
        ::zeroclaw_log::record!(
            INFO,
            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Reproduce with curl to see the exact body: `curl -i -X POST <server>/api/v4/users/login -H 'Content-Type: application/json' -d '{"login_id":"...","password":"..."}'`
  2. Fix the login_id format (email vs username) and password; strip stray whitespace
  3. Prefer a personal access token (`bot_token`) over password login for bots
  4. Read the body in the error message — it states why login was refused

Example fix

# before
[channels.mattermost.team]
base_url = "https://mm.example.com"
login_id = "bot@example.com"
password = "wrong"

# after
[channels.mattermost.team]
base_url = "https://mm.example.com"
bot_token = "<personal access token>"
Defensive patterns

Strategy: try-catch

Validate before calling

let has_token = !bot_token.trim().is_empty();
let can_login = !login_id.trim().is_empty() && !password.trim().is_empty();
if !has_token && !can_login {
    return Err(anyhow::anyhow!("mattermost needs bot_token or login_id+password"));
}

Try / catch

match mm_channel.listen(tx).await {
    Err(e) if e.to_string().starts_with("login failed") => {
        // credentials rejected: alert, do not retry-loop with the same password
    }
    other => other,
}

Prevention

When it happens

Trigger: `bot_token` unset so `token()` falls back to `login()` with the configured login_id and password, and Mattermost rejects the credentials (bad password, unknown login_id, account locked, local logins disabled).

Common situations: Password changed or expired; login_id given as email when the server matches on username (or vice versa); server enforces SSO and refuses local passwords; trailing whitespace/newline in the password from copy-paste.

Related errors


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