{"record":{"id":"54aa12299e7fea78","repo":"zeroclaw-labs/zeroclaw","slug":"post-failed-status-body-54aa12","errorCode":null,"errorMessage":"post failed ({status}): {body}","messagePattern":"post failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/mattermost.rs","lineNumber":740,"sourceCode":"            );\n        }\n\n        let token = self.token().await?;\n        let resp = self\n            .http_client()\n            .post(format!(\"{}/api/v4/posts\", self.base_url))\n            .bearer_auth(token)\n            .json(&body_map)\n            .send()\n            .await?;\n\n        let status = resp.status();\n        if !status.is_success() {\n            let body = resp\n                .text()\n                .await\n                .unwrap_or_else(|e| format!(\"<failed to read response: {e}>\"));\n            bail!(\"post failed ({status}): {body}\");\n        }\n\n        Ok(())\n    }\n\n    async fn listen(&self, tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> Result<()> {\n        match self.listen_mode {\n            MattermostListenMode::Polling => self.listen_polling(tx).await,\n            MattermostListenMode::Websocket => self.listen_websocket(tx).await,\n        }\n    }\n\n    async fn health_check(&self) -> bool {\n        let Ok(token) = self.token().await else {\n            return false;\n        };\n        self.http_client()\n            .get(format!(\"{}/api/v4/users/me\", self.base_url))","sourceCodeStart":722,"sourceCodeEnd":758,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/mattermost.rs#L722-L758","documentation":"A shared POST helper used by `Channel::send` (and other call paths) bails when the HTTP status is not success, including the status and the response body. For sends, the usual meanings are: 403 the bot is not in the target channel, 401 bad/expired token or session, 404 wrong channel/user ID, 400 payload rejected.","triggerScenarios":"Mattermost `send()` posting to a channel the bot has not joined, a typo'd channel or recipient ID, a token revoked since login, or content the server rejects.","commonSituations":"Bot removed from the channel between discovery and send; channel archived; long-lived session token expired mid-run; message exceeds server limits.","solutions":["Read the status and body embedded in the message — Mattermost's body states the reason","403: add the bot to the channel via `POST /api/v4/channels/{channel_id}/members`","401: refresh `bot_token` or let the channel re-login","404: correct the channel ID or recipient (`@username` / user ID)"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if let Err(e) = mm_channel.send(message).await {\n    let msg = e.to_string();\n    if msg.starts_with(\"post failed (403\") {\n        // join channel then retry once\n    } else if msg.starts_with(\"post failed (401\") {\n        // refresh token, do not retry with the same session\n    }\n}","preventionTips":["Ensure the bot is a member of every channel it may reply in before routing messages there","Parse the status code from the error body to route handling (403 join, 401 re-auth, 404 fix ID)","Watch for archived/renamed channels when send failures cluster"],"tags":["mattermost","send","http-status","rust"],"backgroundTag":"http-error-response","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}