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

send failed after token refresh: status={retry_status}, body

Error message

send failed after token refresh: status={retry_status}, body={retry_response}

What it means

send_text_once returned a refresh-indicating result (HTTP 401 or Lark code 99991663); the channel invalidated the cached token, fetched a new tenant_access_token, retried the send — and the retry again returned a refresh-indicating response. Two consecutive auth failures around a fresh token point to a credential or configuration defect, not normal expiry (tokens are cached with TTL-based refresh skew, default 7200s). Retry status and full body are included.

Source

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

        Ok((status, parsed))
    }

    async fn send_json_with_token_refresh(
        &self,
        url: &str,
        token: &mut String,
        body: &serde_json::Value,
        context: &str,
    ) -> anyhow::Result<()> {
        let (status, response) = self.send_text_once(url, token, body).await?;

        if should_refresh_lark_tenant_token(status, &response) {
            self.invalidate_token().await;
            *token = self.get_tenant_access_token().await?;
            let (retry_status, retry_response) = self.send_text_once(url, token, body).await?;

            if should_refresh_lark_tenant_token(retry_status, &retry_response) {
                anyhow::bail!(
                    "send failed after token refresh: status={retry_status}, body={retry_response}"
                );
            }

            ensure_lark_send_success(retry_status, &retry_response, context)?;
        } else {
            ensure_lark_send_success(status, &response, context)?;
        }

        Ok(())
    }

    async fn post_multipart_once(
        &self,
        url: &str,
        token: &str,
        form: Form,
    ) -> anyhow::Result<(reqwest::StatusCode, serde_json::Value)> {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Read body= in the message: code 99991663 with a brand-new token almost always means wrong app credentials.
  2. Re-verify app_id/app_secret against the console and restart so all instances share one canonical config.
  3. Ensure only one process runs per app credential and that api_base is region-consistent.
  4. If credentials are confirmed good, stop retrying immediately on this error (one refresh was already attempted) and cool down.
Defensive patterns

Strategy: retry

Type guard

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

Try / catch

if is_repeat_stale_token(&e) {
    // one refresh already failed: cool down, re-verify config, then retry once later
    lark.invalidate_token().await;
    cooldown(Duration::from_secs(60)).await;
    return lark.send(msg).await;
}

Prevention

When it happens

Trigger: app_secret rotated between the token fetch and its use, token endpoint and message endpoint pointed at different regions via api_base, clock skew or a poisoned token cache, or two channel instances running with different credentials for the same app.

Common situations: A secret rotation in the Lark console while the bot was running; config hot-reload swapping credentials mid-flight; duplicated channel processes (stale and new config) fighting over the same app.

Related errors


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