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

  1. 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.
  2. 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).
  3. Check iLink service status / backend announcements if re-login does not clear it; a 5xx here is server-side.
  4. 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

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


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