{"record":{"id":"e39302b25fe1387a","repo":"zeroclaw-labs/zeroclaw","slug":"sendmessage-failed-status-err","errorCode":null,"errorMessage":"sendMessage failed ({status}): {err}","messagePattern":"sendMessage failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/wechat.rs","lineNumber":1955,"sourceCode":"                \"item_list\": item_list,\n                \"context_token\": context_token.unwrap_or(\"\")\n            },\n            \"base_info\": build_base_info()\n        });\n\n        let resp = self\n            .client\n            .post(self.api_url(\"sendmessage\"))\n            .headers(build_headers(Some(&token)))\n            .json(&body)\n            .timeout(API_TIMEOUT)\n            .send()\n            .await?;\n\n        if !resp.status().is_success() {\n            let status = resp.status();\n            let err = resp.text().await.unwrap_or_default();\n            anyhow::bail!(\"sendMessage failed ({status}): {err}\");\n        }\n\n        // The API reports failures as HTTP 200 with a non-zero ret/errcode\n        // in the body; a status check alone silently drops the message.\n        let body = resp\n            .text()\n            .await\n            .context(\"failed to read sendMessage response body\")?;\n        if let Some(err) = sendmessage_body_error(&body) {\n            anyhow::bail!(\"sendMessage failed ({err})\");\n        }\n\n        Ok(())\n    }\n\n    /// Send a text message via iLink API.\n    async fn send_text(\n        &self,","sourceCodeStart":1937,"sourceCodeEnd":1973,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/wechat.rs#L1937-L1973","documentation":"The WeChat iLink send path checks the HTTP status of the sendMessage request; any non-2xx bails with the status and response body. This is the transport-level send failure; the body-level ret/errcode check (error 345) only runs after this passes.","triggerScenarios":"Calling send()/send_message on the WeChat channel when iLink returns 401 (session token expired), 429 (rate limited), 500/502 (server error), or 404 (wrong endpoint from a misconfigured api_url).","commonSituations":"Long-running bots whose iLink session token lapses; message bursts hitting iLink rate limits; iLink restarts mid-send; proxy or gateway faults.","solutions":["Branch on the embedded status: 401 → re-login the WeChat session; 429 → back off and resend; 5xx → retry; 404 → fix api_url","Retry the send with exponential backoff, but only for transient statuses (429/5xx)","If every send fails, probe a cheap authenticated iLink endpoint to confirm the session is dead","Capture the body text — iLink error strings usually name the exact problem"],"exampleFix":"// before\nchannel.send(msg).await?;\n// after\nlet mut attempt = 0;\nloop {\n    match channel.send(msg.clone()).await {\n        Ok(_) => break,\n        Err(e) if attempt < 3 && is_transient(&e) => {\n            attempt += 1;\n            tokio::time::sleep(Duration::from_secs(1 << attempt)).await;\n        }\n        Err(e) => return Err(e),\n    }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"Inspect the embedded status: retry only 408/429/5xx with exponential backoff; escalate 401 to session re-login and resend once; 404 means config drift — stop and fix api_url.","preventionTips":["Re-authenticate the WeChat session proactively on age thresholds rather than waiting for 401s","Rate-limit outbound sends to stay under iLink limits","Never treat a failed send as delivered — keep messages queued until send returns Ok"],"tags":["wechat","send-message","http","api","outbound"],"backgroundTag":"upstream-api-http-error","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}