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
- 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
- For token codes (99991661/99991663/99991668), force a tenant_access_token refresh and retry once before surfacing the error
- For permission codes, add the missing scope/permission in the Feishu developer console and republish the app version
- 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
- Map Lark numeric codes to actions: token codes -> refresh+retry once; permission codes -> console fix; target codes -> verify chat/open id
- Never judge Lark success by HTTP status alone — always check the body code envelope
- Subscribe to token-expiry errors in logs to detect refresh races early
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
- send failed {context}: status={status}, body={body}
- WS endpoint failed: code={} msg={}
- tenant_access_token request failed: status={status}, body={d
- tenant_access_token failed: {msg}
- bot info failed: code={code}, body={body}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/bb0a9967964e78aa.
Report an issue: GitHub.