{"record":{"id":"cdb0d0106bc8aa27","repo":"zeroclaw-labs/zeroclaw","slug":"send-failed-context-status-status-body-body","errorCode":null,"errorMessage":"send failed {context}: status={status}, body={body}","messagePattern":"send failed (.+?): status=(.+?), body=(.+?)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/lark.rs","lineNumber":509,"sourceCode":"        .unwrap_or(LARK_DEFAULT_TOKEN_TTL.as_secs());\n    ttl.max(1)\n}\n\nfn next_token_refresh_deadline(now: Instant, ttl_seconds: u64) -> Instant {\n    let ttl = Duration::from_secs(ttl_seconds.max(1));\n    let refresh_in = ttl\n        .checked_sub(LARK_TOKEN_REFRESH_SKEW)\n        .unwrap_or(Duration::from_secs(1));\n    now + refresh_in\n}\n\nfn ensure_lark_send_success(\n    status: reqwest::StatusCode,\n    body: &serde_json::Value,\n    context: &str,\n) -> anyhow::Result<()> {\n    if !status.is_success() {\n        anyhow::bail!(\"send failed {context}: status={status}, body={body}\");\n    }\n\n    let code = extract_lark_response_code(body).unwrap_or(0);\n    if code != 0 {\n        anyhow::bail!(\"send failed {context}: code={code}, body={body}\");\n    }\n\n    Ok(())\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum LarkOutgoingMediaKind {\n    Image,\n    File { file_type: &'static str },\n}\n\nimpl LarkOutgoingMediaKind {\n    fn from_marker_kind(kind: &str) -> Option<Self> {","sourceCodeStart":491,"sourceCodeEnd":527,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/lark.rs#L491-L527","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Read the embedded status and body: 401/403 point at token/scope issues, 400 at payload shape, 429 at rate limiting","For 429, retry with backoff — the caller wrappers already refresh tokens, so a pure throttling failure is transient","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)","Confirm the bot is a member of the target chat (or uses the correct receive_id_type) before re-sending"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match ensure_lark_send_success(status, &body, \"send message\").or_else on the caller:\nif let Err(e) = send_json_with_token_refresh(...).await {\n    let s = e.to_string();\n    if s.contains(\"status=429\") || s.contains(\"status=5\") {\n        tokio::time::sleep(backoff.next()).await; // transient: retry\n    } else {\n        // 401/403/400: inspect embedded body, fix token/scope/payload; no blind retry\n    }\n}","preventionTips":["Log and parse the embedded status and body — Lark's HTTP layer gives the precise reason","Retry only 429/5xx with jittered backoff; treat 401/403 as credential/scope problems","Keep tenant_access_token refresh proactive (the wrappers do this) so 401s stay rare"],"tags":["lark","feishu","http-status","api","send"],"backgroundTag":"http-error-response","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}