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
- Read body= in the message: code 99991663 with a brand-new token almost always means wrong app credentials.
- Re-verify app_id/app_secret against the console and restart so all instances share one canonical config.
- Ensure only one process runs per app credential and that api_base is region-consistent.
- 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
- Run exactly one channel instance per Lark app credential.
- On secret rotation, update config and restart atomically.
- Cool down on repeat auth failures instead of hot-looping sends.
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
- bot info request failed after token refresh: status={retry_s
- upload image failed after token refresh: status={retry_statu
- upload file failed after token refresh: status={retry_status
- send failed {context}: status={status}, body={body}
- tenant_access_token request failed: status={status}, body={d
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/2657cc4662c07917.
Report an issue: GitHub.