zeroclaw-labs/zeroclaw · error · anyhow::Error
sendMessage failed ({err})
Error message
sendMessage failed ({err}) What it means
iLink reports sendMessage failures as HTTP 200 with a non-zero ret or errcode inside the JSON body. After reading the body, the channel runs sendmessage_body_error() (wechat.rs:454) and bails with the extracted error string; without this check failures would be silently dropped. Known examples from the crate's tests include 'ret:-1, context token expired' and 'errcode:301, session expired'.
Source
Thrown at crates/zeroclaw-channels/src/wechat.rs:1965
.json(&body)
.timeout(API_TIMEOUT)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let err = resp.text().await.unwrap_or_default();
anyhow::bail!("sendMessage failed ({status}): {err}");
}
// The API reports failures as HTTP 200 with a non-zero ret/errcode
// in the body; a status check alone silently drops the message.
let body = resp
.text()
.await
.context("failed to read sendMessage response body")?;
if let Some(err) = sendmessage_body_error(&body) {
anyhow::bail!("sendMessage failed ({err})");
}
Ok(())
}
/// Send a text message via iLink API.
async fn send_text(
&self,
to: &str,
text: &str,
context_token: Option<&str>,
) -> anyhow::Result<()> {
self.send_message_items(
to,
vec![serde_json::json!({
"type": ITEM_TYPE_TEXT,
"text_item": { "text": markdown_to_plain_text(text) }
})],View on GitHub (pinned to 88bb9c8533)
Solutions
- Parse the embedded error text: token/session errors (e.g. 'context token expired', 'session expired') → re-login and resend once; target-invalid → drop and log; content rejection → adjust the payload
- Re-authenticate the iLink session before retrying when the error mentions tokens or sessions
- Dead-letter the message after a bounded number of in-band failures instead of blind-retrying
- If the error text looks like HTML or is empty, suspect a proxy mangling responses
Example fix
// before
channel.send(msg).await?; // HTTP 200 hides ret=-1 'context token expired'
// after
if let Err(e) = channel.send(msg.clone()).await {
if e.to_string().contains("token expired") || e.to_string().contains("session expired") {
channel.relogin().await?;
channel.send(msg).await?;
} else { return Err(e); }
} Defensive patterns
Strategy: try-catch
Try / catch
Catch the send error and branch on the trailing err text: token/session errors → re-login and resend once; target-invalid/blocked → drop and log; unknown → dead-letter with the raw body attached for diagnosis.
Prevention
- Validate chat targets against the known contact/chat list before sending
- Treat HTTP 200 as unproven — always surface in-band ret/errcode failures
- Log full error strings to build a retry/no-retry decision table for iLink codes
When it happens
Trigger: An HTTP-200 sendMessage response whose JSON body has ret != 0 or errcode != 0 — session/token expiry surfaced in-band, invalid chat target, blocked recipient, or content rejected upstream.
Common situations: iLink session expiring while HTTP still succeeds; sending to a chat the bot left or was removed from; recipient blocked the bot; middleware or version drift changing the error body shape (empty or non-JSON bodies are treated as legacy success and do NOT trigger this).
Related errors
- sendMessage failed ({status}): {err}
- WeCom API error (errcode={errcode}): {errmsg}
- Discord send message failed ({status}): {err}
- Cannot persist empty {channel_type} identity
- WeChat channel requires the `channel-wechat` feature
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/9c7881c1f2e9aaf2.
Report an issue: GitHub.