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

bot info request failed after token refresh: status={retry_s

Error message

bot info request failed after token refresh: status={retry_status}, body={retry_body}

What it means

During bot info discovery (GET bot/v3/info), the first attempt looked like a stale token (HTTP 401 or Lark code 99991663), so the channel invalidated its cached token, fetched a fresh tenant_access_token, and retried — and the retry still returned non-2xx. A failure immediately after a successful token refresh usually means the problem is not token expiry: the request itself is being rejected at the HTTP layer. The retry status and body are included.

Source

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

            .await?;
        let status = resp.status();
        let body = resp
            .json::<serde_json::Value>()
            .await
            .unwrap_or_else(|_| serde_json::json!({}));
        Ok((status, body))
    }

    async fn refresh_bot_open_id(&self) -> anyhow::Result<Option<String>> {
        let token = self.get_tenant_access_token().await?;
        let (status, body) = self.fetch_bot_open_id_with_token(&token).await?;

        let body = if should_refresh_lark_tenant_token(status, &body) {
            self.invalidate_token().await;
            let refreshed = self.get_tenant_access_token().await?;
            let (retry_status, retry_body) = self.fetch_bot_open_id_with_token(&refreshed).await?;
            if !retry_status.is_success() {
                anyhow::bail!(
                    "bot info request failed after token refresh: status={retry_status}, body={retry_body}"
                );
            }
            retry_body
        } else {
            if !status.is_success() {
                anyhow::bail!("bot info request failed: status={status}, body={body}");
            }
            body
        };

        let code = body.get("code").and_then(|c| c.as_i64()).unwrap_or(-1);
        if code != 0 {
            anyhow::bail!("bot info failed: code={code}, body={body}");
        }

        let bot_open_id = body
            .pointer("/bot/open_id")

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Read retry_status and retry_body in the message for the exact upstream rejection.
  2. Confirm the app has the Robot (bot) capability enabled and required scopes granted, then re-publish the app version.
  3. Verify api_base is consistent for both token and API calls and matches the app's region.
  4. If retry_status is 5xx or 429, wait and retry later; check the Lark status page.
Defensive patterns

Strategy: fallback

Type guard

fn is_post_refresh_failure(err: &anyhow::Error) -> bool {
    let s = err.to_string();
    s.contains("after token refresh")
}

Try / catch

if is_post_refresh_failure(&e) {
    // A refresh was already attempted and failed: degrade instead of looping
    channel.set_degraded("lark auth unstable").await;
    queue_for_later_retry(msg);
}

Prevention

When it happens

Trigger: A freshly minted token still fails bot/v3/info: api_base routes the token endpoint and the API call to different/mismatched regions, a proxy rejects the bot info path, the app lacks bot capability so the endpoint is unavailable, or a Lark 5xx incident straddles both attempts.

Common situations: Mixed-region configuration (token works on one domain, bot info blocked on another), permission or capability changes on the app mid-run, corporate proxies allowing some paths but not others, or an ongoing Lark open-platform incident.

Related errors


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