{"record":{"id":"0b9a49c84758d7d6","repo":"zeroclaw-labs/zeroclaw","slug":"getuploadurl-failed-status-body","errorCode":null,"errorMessage":"getUploadUrl failed ({status}): {body}","messagePattern":"getUploadUrl failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/wechat.rs","lineNumber":1295,"sourceCode":"            \"filesize\": aes_ecb_padded_size(payload.bytes.len()),\n            \"no_need_thumb\": true,\n            \"aeskey\": hex::encode(aes_key),\n            \"base_info\": build_base_info()\n        });\n\n        let resp = self\n            .client\n            .post(self.api_url(\"getuploadurl\"))\n            .headers(build_headers(Some(&token)))\n            .json(&body)\n            .timeout(API_TIMEOUT)\n            .send()\n            .await?;\n\n        if !resp.status().is_success() {\n            let status = resp.status();\n            let body = resp.text().await.unwrap_or_default();\n            anyhow::bail!(\"getUploadUrl failed ({status}): {body}\");\n        }\n\n        let data: serde_json::Value = resp.json().await?;\n        data.get(\"upload_param\")\n            .and_then(|value| value.as_str())\n            .filter(|value| !value.is_empty())\n            .map(str::to_string)\n            .context(\"getUploadUrl returned no upload_param\")\n    }\n\n    async fn upload_to_cdn(\n        &self,\n        upload_param: &str,\n        filekey: &str,\n        ciphertext: &[u8],\n    ) -> anyhow::Result<String> {\n        let url = self.cdn_upload_url(upload_param, filekey);\n        let mut last_error: Option<anyhow::Error> = None;","sourceCodeStart":1277,"sourceCodeEnd":1313,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/wechat.rs#L1277-L1313","documentation":"`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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: fire-and-forget attachment send\nchannel.send(msg_with_attachment(\"chart.png\")).await?; // -> getUploadUrl failed (401 Unauthorized): ...\n\n// after: on upload-param auth failure, re-login once and retry\nmatch channel.send(msg_with_attachment(\"chart.png\")).await {\n    Err(err) if err.to_string().contains(\"getUploadUrl failed (40\") => {\n        channel.qr_login().await?; // fresh token via QR scan\n        channel.send(msg_with_attachment(\"chart.png\")).await?\n    }\n    other => other?,\n}","handlingStrategy":"retry","validationCode":"// ensure a live session before the first attachment send of a batch\nif channel.token_age_exceeds(Duration::from_secs(6 * 3600)) {\n    channel.qr_login().await?; // refresh token proactively\n}\n// optionally preflight the upload endpoint with a tiny probe payload","typeGuard":null,"tryCatchPattern":"match channel.send(msg_with_attachment(rel)).await {\n    Err(err) => {\n        let msg = err.to_string();\n        if msg.contains(\"getUploadUrl failed (40\") {\n            channel.qr_login().await?; // expired/invalid token: re-login once\n            channel.send(msg_with_attachment(rel)).await?\n        } else if msg.contains(\"getUploadUrl failed (5\") || msg.contains(\"getUploadUrl failed (429\") {\n            tokio::time::sleep(std::time::Duration::from_secs(3)).await;\n            channel.send(msg_with_attachment(rel)).await?\n        } else {\n            return Err(err);\n        }\n    }\n    Ok(_) => Ok(()),\n}","preventionTips":["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."],"tags":["wechat","api","upload","authentication","attachment","http"],"backgroundTag":"api-auth-expired","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}