zeroclaw-labs/zeroclaw · critical · anyhow::Error
tenant_access_token request failed: status={status}, body={d
Error message
tenant_access_token request failed: status={status}, body={data} What it means
Thrown by LarkChannel::get_tenant_access_token when the POST to {api_base}/auth/v3/tenant_access_token/internal returns a non-2xx HTTP status. This is the first authentication step for the Lark (Feishu/Lark Suite) channel: every send, upload, and audio fetch depends on the tenant_access_token it returns. The full upstream response body is embedded in the message, so the real reason is visible. Transport-level failures (DNS, TLS, connect) are NOT this error; they surface earlier as reqwest::Error via the `?` on send().
Source
Thrown at crates/zeroclaw-channels/src/lark.rs:1497
if let Some(ref token) = *cached
&& Instant::now() < token.refresh_after
{
return Ok(token.value.clone());
}
}
let url = self.tenant_access_token_url();
let body = serde_json::json!({
"app_id": self.app_id,
"app_secret": self.app_secret,
});
let resp = self.http_client().post(&url).json(&body).send().await?;
let status = resp.status();
let data: serde_json::Value = resp.json().await?;
if !status.is_success() {
anyhow::bail!("tenant_access_token request failed: status={status}, body={data}");
}
let code = data.get("code").and_then(|c| c.as_i64()).unwrap_or(-1);
if code != 0 {
let msg = data
.get("msg")
.and_then(|m| m.as_str())
.unwrap_or("unknown error");
anyhow::bail!("tenant_access_token failed: {msg}");
}
let token = data
.get("tenant_access_token")
.and_then(|t| t.as_str())
.ok_or_else(|| {
::zeroclaw_log::record!(
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)View on GitHub (pinned to 88bb9c8533)
Solutions
- Read the body=... part of the message; Lark's JSON body states the upstream reason.
- Verify api_base matches the app's region: open.feishu.cn for Feishu, open.larksuite.com for Lark Suite.
- Reproduce manually: curl -X POST <api_base>/auth/v3/tenant_access_token/internal -H 'Content-Type: application/json' -d '{"app_id":"...","app_secret":"..."}' and compare.
- Check HTTP(S)_PROXY / HTTPS_PROXY env vars and firewall egress rules for the channel process.
- If status is 5xx, check the Lark/Feishu status page and retry later.
Example fix
// before (config, app created on Feishu China) [channels.lark.main] api_base = "https://open.larksuite.com" // after [channels.lark.main] api_base = "https://open.feishu.cn"
Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-flight the token endpoint before wiring the channel into message flow
async fn lark_token_preflight(client: &reqwest::Client, api_base: &str, app_id: &str, app_secret: &str) -> anyhow::Result<()> {
let url = format!("{api_base}/auth/v3/tenant_access_token/internal");
let resp = client.post(&url).json(&serde_json::json!({"app_id": app_id, "app_secret": app_secret})).send().await?;
anyhow::ensure!(resp.status().is_success(), "token preflight failed: {}", resp.status());
Ok(())
} Try / catch
match channel.send(msg).await {
Ok(_) => {}
Err(e) if e.to_string().contains("tenant_access_token request failed") => {
let retryable = e.to_string().contains("status=5") || e.to_string().contains("status=429");
if retryable { backoff_retry().await } else { mark_channel_unhealthy(&e) }
}
Err(e) => return Err(e),
} Prevention
- Validate api_base region (feishu.cn vs larksuite.com) once at startup with a token preflight.
- Alert on 4xx vs 5xx token-fetch failures separately: 4xx is config, 5xx is upstream.
- Keep credentials and api_base in one canonical config section; avoid per-deploy overrides.
When it happens
Trigger: POSTing app_id/app_secret to /auth/v3/tenant_access_token/internal and getting 4xx/5xx: 404 from a wrong api_base domain (open.feishu.cn vs open.larksuite.com region mismatch), 400 from a rewritten request path, 401/403 from an egress proxy, or 5xx during a Lark open-platform incident.
Common situations: api_base configured for the wrong region (Feishu China vs Lark Suite international), corporate proxies or TLS-inspecting middleboxes mangling the request, Lark-side outages, or a typo'd custom api_base in [channels.lark.<alias>].
Related errors
- tenant_access_token failed: {msg}
- send failed {context}: status={status}, body={body}
- send failed {context}: code={code}, body={body}
- WS endpoint failed: code={} msg={}
- bot info request failed after token refresh: status={retry_s
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/b52b1b3e64e77b2d.
Report an issue: GitHub.