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

  1. Read the body=... part of the message; Lark's JSON body states the upstream reason.
  2. Verify api_base matches the app's region: open.feishu.cn for Feishu, open.larksuite.com for Lark Suite.
  3. 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.
  4. Check HTTP(S)_PROXY / HTTPS_PROXY env vars and firewall egress rules for the channel process.
  5. 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

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


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