zeroclaw-labs/zeroclaw · error · anyhow::Error

send failed {context}: status={status}, body={body}

Error message

send failed {context}: status={status}, body={body}

What it means

ensure_lark_send_success is the shared gate for every Lark/Feishu outbound HTTP call (message send, image/file upload, approval request); its first branch bails when the HTTP status is not 2xx. The context string in the message names the failing operation, and the raw response body is embedded, so the Lark open-platform HTTP error (401 bad token, 400 bad payload, 403 no permission, 429 rate limit) is visible verbatim.

Source

Thrown at crates/zeroclaw-channels/src/lark.rs:509

        .unwrap_or(LARK_DEFAULT_TOKEN_TTL.as_secs());
    ttl.max(1)
}

fn next_token_refresh_deadline(now: Instant, ttl_seconds: u64) -> Instant {
    let ttl = Duration::from_secs(ttl_seconds.max(1));
    let refresh_in = ttl
        .checked_sub(LARK_TOKEN_REFRESH_SKEW)
        .unwrap_or(Duration::from_secs(1));
    now + refresh_in
}

fn ensure_lark_send_success(
    status: reqwest::StatusCode,
    body: &serde_json::Value,
    context: &str,
) -> anyhow::Result<()> {
    if !status.is_success() {
        anyhow::bail!("send failed {context}: status={status}, body={body}");
    }

    let code = extract_lark_response_code(body).unwrap_or(0);
    if code != 0 {
        anyhow::bail!("send failed {context}: code={code}, body={body}");
    }

    Ok(())
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LarkOutgoingMediaKind {
    Image,
    File { file_type: &'static str },
}

impl LarkOutgoingMediaKind {
    fn from_marker_kind(kind: &str) -> Option<Self> {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Read the embedded status and body: 401/403 point at token/scope issues, 400 at payload shape, 429 at rate limiting
  2. For 429, retry with backoff — the caller wrappers already refresh tokens, so a pure throttling failure is transient
  3. For 403/400, check that the app has the required scope (im:message, im:resource, approval etc.) and that the payload matches the API version for your region (feishu vs larksuite domain)
  4. Confirm the bot is a member of the target chat (or uses the correct receive_id_type) before re-sending
Defensive patterns

Strategy: try-catch

Try / catch

match ensure_lark_send_success(status, &body, "send message").or_else on the caller:
if let Err(e) = send_json_with_token_refresh(...).await {
    let s = e.to_string();
    if s.contains("status=429") || s.contains("status=5") {
        tokio::time::sleep(backoff.next()).await; // transient: retry
    } else {
        // 401/403/400: inspect embedded body, fix token/scope/payload; no blind retry
    }
}

Prevention

When it happens

Trigger: Any of send_json_with_token_refresh, upload_lark_image, upload_lark_file or request_approval_attributed receives a non-success status from open.feishu.cn / open.larksuite.com: tenant_access_token expired or invalid yields 401, malformed request JSON yields 400, missing app scope yields 403, bursts yield 429.

Common situations: App credentials rotated so cached tenant tokens fail; sending to a chat_id the bot was never added to; uploading a file type/size the API rejects; QPS spike across multiple channels tripping Lark rate limits.

Related errors


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