{"record":{"id":"e714b9f817c479ef","repo":"zeroclaw-labs/zeroclaw","slug":"login-failed-status-body","errorCode":null,"errorMessage":"login failed ({status}): {body}","messagePattern":"login failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/mattermost.rs","lineNumber":318,"sourceCode":"\n    /// Perform the Mattermost password login flow and return the session\n    /// token. The session token is returned via the `Token` response header\n    /// per Mattermost API v4.\n    async fn login(&self, login_id: &str, password: &str) -> Result<String> {\n        let resp = self\n            .http_client()\n            .post(format!(\"{}/api/v4/users/login\", self.base_url))\n            .json(&serde_json::json!({\n                \"login_id\": login_id,\n                \"password\": password,\n            }))\n            .send()\n            .await\n            .context(\"login request failed\")?;\n        if !resp.status().is_success() {\n            let status = resp.status();\n            let body = resp.text().await.unwrap_or_default();\n            bail!(\"login failed ({status}): {body}\");\n        }\n        let token = resp\n            .headers()\n            .get(\"Token\")\n            .and_then(|v| v.to_str().ok())\n            .ok_or_else(|| {\n                ::zeroclaw_log::record!(\n                    ERROR,\n                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)\n                        .with_outcome(::zeroclaw_log::EventOutcome::Failure),\n                    \"login succeeded but the response had no Token header\"\n                );\n                anyhow::Error::msg(\"login succeeded but the response had no Token header\")\n            })?\n            .to_string();\n        ::zeroclaw_log::record!(\n            INFO,\n            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),","sourceCodeStart":300,"sourceCodeEnd":336,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/mattermost.rs#L300-L336","documentation":"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.","triggerScenarios":"`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).","commonSituations":"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.","solutions":["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\":\"...\"}'`","Fix the login_id format (email vs username) and password; strip stray whitespace","Prefer a personal access token (`bot_token`) over password login for bots","Read the body in the error message — it states why login was refused"],"exampleFix":"# before\n[channels.mattermost.team]\nbase_url = \"https://mm.example.com\"\nlogin_id = \"bot@example.com\"\npassword = \"wrong\"\n\n# after\n[channels.mattermost.team]\nbase_url = \"https://mm.example.com\"\nbot_token = \"<personal access token>\"","handlingStrategy":"try-catch","validationCode":"let has_token = !bot_token.trim().is_empty();\nlet can_login = !login_id.trim().is_empty() && !password.trim().is_empty();\nif !has_token && !can_login {\n    return Err(anyhow::anyhow!(\"mattermost needs bot_token or login_id+password\"));\n}","typeGuard":null,"tryCatchPattern":"match mm_channel.listen(tx).await {\n    Err(e) if e.to_string().starts_with(\"login failed\") => {\n        // credentials rejected: alert, do not retry-loop with the same password\n    }\n    other => other,\n}","preventionTips":["Prefer bot_token (personal access token) over password login for bots","If password login is required, verify credentials with curl before deploy","Alert on login failures instead of retrying — repeated failures can trip account lockout"],"tags":["mattermost","login","auth","http-status","rust"],"backgroundTag":"invalid-credentials","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}