zeroclaw-labs/zeroclaw · error · anyhow::Error
upload image failed after token refresh: status={retry_statu
Error message
upload image failed after token refresh: status={retry_status}, body={retry_response} What it means
The image upload path hit a refresh-indicating result (401/99991663), rebuilt the multipart form, fetched a fresh tenant_access_token, retried post_multipart_once — and the retry again signaled an invalid token. Structurally identical to error 108 but on the image upload endpoint, including a rebuilt form via build_lark_image_upload_form. Retry status and body are included.
Source
Thrown at crates/zeroclaw-channels/src/lark.rs:2222
Ok((status, parsed))
}
async fn upload_lark_image(
&self,
token: &mut String,
marker: &LarkResolvedMediaMarker,
) -> anyhow::Result<String> {
let url = format!("{}/im/v1/images", self.api_base());
let form = build_lark_image_upload_form(marker).await?;
let (status, response) = self.post_multipart_once(&url, token, form).await?;
let response = if should_refresh_lark_tenant_token(status, &response) {
self.invalidate_token().await;
*token = self.get_tenant_access_token().await?;
let retry_form = build_lark_image_upload_form(marker).await?;
let (retry_status, retry_response) =
self.post_multipart_once(&url, token, retry_form).await?;
if should_refresh_lark_tenant_token(retry_status, &retry_response) {
anyhow::bail!(
"upload image failed after token refresh: status={retry_status}, body={retry_response}"
);
}
ensure_lark_send_success(retry_status, &retry_response, "upload image")?;
retry_response
} else {
ensure_lark_send_success(status, &response, "upload image")?;
response
};
response
.pointer("/data/image_key")
.or_else(|| response.get("image_key"))
.and_then(|v| v.as_str())
.map(str::to_string)
.ok_or_else(|| anyhow::Error::msg("Lark/Feishu image upload returned no image_key"))
}
View on GitHub (pinned to 88bb9c8533)
Solutions
- Verify app credentials and restart the channel (see error 108 solutions).
- Confirm the app has the image upload scope granted and the app version published.
- Check the body code in the message; if it is not 99991663, the failure is permission-side, not token-side.
- Do not add another refresh loop on top; one refresh already happened and failed.
Defensive patterns
Strategy: retry
Type guard
fn is_repeat_stale_token(err: &anyhow::Error) -> bool {
err.to_string().contains("upload image failed after token refresh")
} Try / catch
if is_repeat_stale_token(&e) {
cooldown(Duration::from_secs(60)).await; // refresh already attempted once
return lark.send(msg).await; // single later retry, not a loop
} Prevention
- Keep one canonical credential source per app; duplicates cause revocation confusion.
- Alert when a freshly refreshed token is rejected — that is a config incident, not noise.
- Send text fallbacks when image upload stays broken so conversations continue.
When it happens
Trigger: Same credential/region defects as 108, plus cases where the freshly fetched token is rejected because the app lacks image-upload permission or the token belongs to a different app than the upload request implies.
Common situations: Secret rotation mid-run, mismatched api_base regions, or concurrent channel instances with divergent credentials for one Lark app.
Related errors
- upload file failed after token refresh: status={retry_status
- send failed after token refresh: status={retry_status}, body
- tenant_access_token request failed: status={status}, body={d
- tenant_access_token failed: {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/9990877eabe8bc73.
Report an issue: GitHub.