zeroclaw-labs/zeroclaw · error

WeCom API error (errcode={errcode}): {errmsg}

Error message

WeCom API error (errcode={errcode}): {errmsg}

What it means

WeCom webhooks answer HTTP 200 with {"errcode":N,"errmsg":"..."}; errcode != 0 is a failure and bails with both values. A missing errcode field parses as -1, so a malformed or non-JSON body also trips this with errcode=-1 and errmsg='unknown error'.

Source

Thrown at crates/zeroclaw-channels/src/wecom.rs:105

            .json(&body)
            .send()
            .await?;

        if !resp.status().is_success() {
            let status = resp.status();
            let err = resp.text().await.unwrap_or_default();
            anyhow::bail!("WeCom webhook send failed ({status}): {err}");
        }

        // WeCom returns {"errcode":0,"errmsg":"ok"} on success.
        let result: serde_json::Value = resp.json().await?;
        let errcode = result.get("errcode").and_then(|v| v.as_i64()).unwrap_or(-1);
        if errcode != 0 {
            let errmsg = result
                .get("errmsg")
                .and_then(|v| v.as_str())
                .unwrap_or("unknown error");
            anyhow::bail!("WeCom API error (errcode={errcode}): {errmsg}");
        }

        Ok(())
    }

    async fn listen(&self, tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> anyhow::Result<()> {
        ::zeroclaw_log::record!(
            INFO,
            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
            "channel ready (send-only via Bot Webhook)"
        );
        tx.closed().await;
        Ok(())
    }

    async fn health_check(&self) -> bool {
        // Verify we can reach the WeCom API endpoint.
        let resp = self

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Match the errcode against WeCom webhook docs: 45002 → shorten the message; 45009 → throttle outbound sends and retry; key errors → regenerate the webhook URL and update config
  2. For errcode=-1 with odd errmsg, dump the raw response — something between the bot and WeCom mangled the body
  3. Add per-robot send throttling to stay under frequency limits
  4. Retry once after the appropriate backoff, then dead-letter

Example fix

// before
for m in burst { channel.send(m).await?; } // trips 45009 frequency limit
// after
for m in burst {
    channel.send(m).await?;
    tokio::time::sleep(Duration::from_secs(3)).await; // stay under robot rate limit
}
Defensive patterns

Strategy: try-catch

Try / catch

Extract errcode from the message via 'errcode=(\d+)' and branch: 45009 → sleep past the rate window and retry once; 45002 → chunk and resend; key errors → disable channel; errcode=-1 → log the raw body (proxy interference) and do not retry.

Prevention

When it happens

Trigger: send()/health_check() returning errcode such as 45002 (message content over the length limit), 45009 (send frequency limit hit), an invalid-key error, or errcode=-1 because the response body was not the expected JSON envelope.

Common situations: Bot bursting past the WeCom robot rate limit; markdown/text over the content limit; stale or revoked webhook key; a proxy/WAF returning HTML that still yields HTTP 200.

Related errors


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