zeroclaw-labs/zeroclaw · error · anyhow::Error
getUploadUrl failed ({status}): {body}
Error message
getUploadUrl failed ({status}): {body} What it means
`request_upload_param` is the first step of the WeChat media upload pipeline: it POSTs filekey, sizes, MD5, and the AES key to the iLink `getuploadurl` endpoint using the channel's bot token. This bail means the endpoint answered with a non-2xx status, and the message embeds both status and response body. The most common cause is an expired or invalidated bot token, since the token is only fetched via `get_token()` immediately before the request.
Source
Thrown at crates/zeroclaw-channels/src/wechat.rs:1295
"filesize": aes_ecb_padded_size(payload.bytes.len()),
"no_need_thumb": true,
"aeskey": hex::encode(aes_key),
"base_info": build_base_info()
});
let resp = self
.client
.post(self.api_url("getuploadurl"))
.headers(build_headers(Some(&token)))
.json(&body)
.timeout(API_TIMEOUT)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
anyhow::bail!("getUploadUrl failed ({status}): {body}");
}
let data: serde_json::Value = resp.json().await?;
data.get("upload_param")
.and_then(|value| value.as_str())
.filter(|value| !value.is_empty())
.map(str::to_string)
.context("getUploadUrl returned no upload_param")
}
async fn upload_to_cdn(
&self,
upload_param: &str,
filekey: &str,
ciphertext: &[u8],
) -> anyhow::Result<String> {
let url = self.cdn_upload_url(upload_param, filekey);
let mut last_error: Option<anyhow::Error> = None;View on GitHub (pinned to 88bb9c8533)
Solutions
- Re-login the channel (run `qr_login()` again and scan) to mint a fresh token, then retry the send — expired-token is the most frequent cause.
- Read the embedded status/body: 401/403 points at auth (re-login), 400 at payload validation (check kind/media_type and sizes), 5xx/429 at transient backend conditions (retry with backoff).
- Check iLink service status / backend announcements if re-login does not clear it; a 5xx here is server-side.
- If it reproduces consistently after re-login with a 4xx, capture the request body (filekey, rawsize, rawfilemd5, filesize fields) and compare against the current iLink bot API expectations.
Example fix
// before: fire-and-forget attachment send
channel.send(msg_with_attachment("chart.png")).await?; // -> getUploadUrl failed (401 Unauthorized): ...
// after: on upload-param auth failure, re-login once and retry
match channel.send(msg_with_attachment("chart.png")).await {
Err(err) if err.to_string().contains("getUploadUrl failed (40") => {
channel.qr_login().await?; // fresh token via QR scan
channel.send(msg_with_attachment("chart.png")).await?
}
other => other?,
} Defensive patterns
Strategy: retry
Validate before calling
// ensure a live session before the first attachment send of a batch
if channel.token_age_exceeds(Duration::from_secs(6 * 3600)) {
channel.qr_login().await?; // refresh token proactively
}
// optionally preflight the upload endpoint with a tiny probe payload Try / catch
match channel.send(msg_with_attachment(rel)).await {
Err(err) => {
let msg = err.to_string();
if msg.contains("getUploadUrl failed (40") {
channel.qr_login().await?; // expired/invalid token: re-login once
channel.send(msg_with_attachment(rel)).await?
} else if msg.contains("getUploadUrl failed (5") || msg.contains("getUploadUrl failed (429") {
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
channel.send(msg_with_attachment(rel)).await?
} else {
return Err(err);
}
}
Ok(_) => Ok(()),
} Prevention
- Re-login on a schedule or before long batch upload runs instead of waiting for the first 401.
- Retry 5xx/429 with backoff but never blind-retry 4xx validation errors; inspect the embedded body first.
- Log out other sessions for the bot account if tokens keep being invalidated server-side.
- On persistent 400s after a backend update, verify the request contract (filekey, rawsize/rawfilemd5, media_type) still matches the current iLink bot API.
When it happens
Trigger: Any attachment send (image/file/video/audio to a WeChat user) where POST {api_base}/getuploadurl returns an error status: 401/403-style rejections when the ilink_bot_token expired or the bot was logged out, 4xx for malformed/missing fields (bad filekey, size/md5 mismatch vs. what a backend version now validates), 5xx during iLink outages, or rate limiting. `get_token()` succeeding only means a cached token exists — it can still be stale server-side.
Common situations: Long-running bots whose cached token outlives the server-side session (bot logged in elsewhere, WeChat account re-authorized, device kicked); iLink API version changes tightening request validation; transient backend errors during WeChat service windows; test environments with fake tokens; sending attachments after the QR login flow silently degraded.
Related errors
- attachment download failed ({status}): {body}
- WeChat QR code fetch failed ({$status}): {$body}
- sendMessage failed ({status}): {err}
- Failed to initiate call: {}
- 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/0b9a49c84758d7d6.
Report an issue: GitHub.