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

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

Error message

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

What it means

The second branch of ensure_lark_send_success: Lark's open API returns HTTP 200 but carries a business status in the JSON body; extract_lark_response_code pulls that code (defaulting 0) and any non-zero code bails with the code and full body. This is Lark's standard envelope (code/msg/data), so an HTTP-level success can still encode 'invalid token' (99991661/99991663), 'no permission' (230001), 'IP not whitelisted' (230002) and similar.

Source

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

    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> {
        match kind.trim().to_ascii_uppercase().as_str() {
            "IMAGE" | "PHOTO" => Some(Self::Image),
            "DOCUMENT" | "FILE" => Some(Self::File {
                file_type: "stream",
            }),

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Look up the numeric code in Lark's error-code docs — the message embeds both code and body, which pinpoints the exact API-level refusal
  2. For token codes (99991661/99991663/99991668), force a tenant_access_token refresh and retry once before surfacing the error
  3. For permission codes, add the missing scope/permission in the Feishu developer console and republish the app version
  4. For 'not found' target codes, verify the chat_id/open_id and that the bot is still in the conversation
Defensive patterns

Strategy: try-catch

Try / catch

if let Err(e) = upload_lark_image(...).await {
    let s = e.to_string();
    if s.contains("code=99991663") || s.contains("code=99991661") {
        // token expired/invalid: force refresh, retry once
    } else if s.contains("code=230002") {
        // whitelist the egress IP in the Lark console — config action, not code
    } else {
        return Err(e); // permission/not-found: fix app scopes or target id
    }
}

Prevention

When it happens

Trigger: send_json_with_token_refresh, upload_lark_image, upload_lark_file or request_approval_attributed gets a 200 whose body code is non-zero — e.g. tenant_access_token just expired between refresh and call, the app lacks the specific API scope, or the target id (chat_id/open_id) does not exist for that app.

Common situations: Token expiry races under load (refresh later in the pipeline); partially granted scopes — app can send text but not upload files; sending to a chat the bot left; environment IP addresses not whitelisted in the Lark console.

Related errors


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