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

  1. 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
  2. Re-authenticate the iLink session before retrying when the error mentions tokens or sessions
  3. Dead-letter the message after a bounded number of in-band failures instead of blind-retrying
  4. 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

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


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