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

Lark add_reaction failed for {message_id}: status={status},

Error message

Lark add_reaction failed for {message_id}: status={status}, body={err_body}

What it means

Adding an emoji reaction (POST /im/v1/messages/{message_id}/reactions) returned non-2xx. The message includes the message_id, status, and the raw body text, mapping directly to Lark's error catalog. Reactions are cosmetic acknowledgments, so callers usually treat this as non-fatal; the code!=0 variant (200 with business code) is checked separately right after this branch.

Source

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

        let mut token = self.get_tenant_access_token().await?;

        let mut retried = false;
        loop {
            let response = self
                .post_message_reaction_with_token(message_id, &token, emoji_type)
                .await?;

            if response.status().as_u16() == 401 && !retried {
                self.invalidate_token().await;
                token = self.get_tenant_access_token().await?;
                retried = true;
                continue;
            }

            if !response.status().is_success() {
                let status = response.status();
                let err_body = response.text().await.unwrap_or_default();
                anyhow::bail!(
                    "Lark add_reaction failed for {message_id}: status={status}, body={err_body}"
                );
            }

            let payload: serde_json::Value = response.json().await?;
            let code = payload.get("code").and_then(|v| v.as_i64()).unwrap_or(-1);
            if code != 0 {
                let msg = payload
                    .get("msg")
                    .and_then(|v| v.as_str())
                    .unwrap_or("unknown error");
                ::zeroclaw_log::record!(
                    WARN,
                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
                        .with_outcome(::zeroclaw_log::EventOutcome::Unknown)
                        .with_attrs(::serde_json::json!({
                            "code": code,
                            "message_id": message_id,

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Read body= in the message and map the Lark code: permission -> grant the reaction scope and republish; invalid emoji -> use a standard emoji_type key.
  2. If the bot was removed from the chat, skip reacting for that chat.
  3. Treat as best-effort: log and continue rather than failing the message pipeline.
  4. Rate-limit reaction calls if 429.
Defensive patterns

Strategy: try-catch

Type guard

fn is_add_reaction_failure(err: &anyhow::Error) -> bool {
    err.to_string().contains("Lark add_reaction failed")
}

Try / catch

if let Err(e) = lark.add_reaction(&message_id, emoji).await {
    if is_add_reaction_failure(&e) {
        tracing::debug!(error = %e, "reaction skipped (cosmetic)"); // never fail the turn
    } else {
        return Err(e);
    }
}

Prevention

When it happens

Trigger: 404 for a message too old, deleted, or in a chat the bot left; 403 when the app lacks the add-reaction scope; 400 for an emoji_type not in Lark's allowed emoji key set; 429 from reaction bursts.

Common situations: Bot removed from the group before the reaction fires, permission changes dropping the reaction scope, or an emoji_type value that Lark does not recognize (custom vs standard emoji keys).

Related errors


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