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

  1. Verify app credentials and restart the channel (see error 108 solutions).
  2. Confirm the app has the image upload scope granted and the app version published.
  3. Check the body code in the message; if it is not 99991663, the failure is permission-side, not token-side.
  4. 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

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


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