{"record":{"id":"d20afee00e155a53","repo":"zeroclaw-labs/zeroclaw","slug":"attachment-exceeds-mb-limit","errorCode":null,"errorMessage":"attachment exceeds {} MB limit","messagePattern":"attachment exceeds (.+?) MB limit","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/wechat.rs","lineNumber":1206,"sourceCode":"\n        if let Some(len) = resp.content_length()\n            && len > WECHAT_MEDIA_MAX_BYTES\n        {\n            anyhow::bail!(\n                \"attachment Content-Length ({len} bytes) exceeds {} MB limit\",\n                WECHAT_MEDIA_MAX_BYTES / (1024 * 1024)\n            );\n        }\n\n        let content_type = resp\n            .headers()\n            .get(reqwest::header::CONTENT_TYPE)\n            .and_then(|value| value.to_str().ok())\n            .map(str::to_string);\n        let bytes = resp.bytes().await?.to_vec();\n\n        if bytes.len() as u64 > WECHAT_MEDIA_MAX_BYTES {\n            anyhow::bail!(\n                \"attachment exceeds {} MB limit\",\n                WECHAT_MEDIA_MAX_BYTES / (1024 * 1024)\n            );\n        }\n\n        Ok(WeChatMediaPayload {\n            file_name: self.remote_file_name(url, content_type.as_deref(), kind),\n            bytes,\n        })\n    }\n\n    async fn load_attachment_payload(\n        &self,\n        attachment: &WeChatAttachment,\n    ) -> anyhow::Result<WeChatMediaPayload> {\n        let target = attachment.target.trim();\n        if is_remote_url(target) {\n            return self","sourceCodeStart":1188,"sourceCodeEnd":1224,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/wechat.rs#L1188-L1224","documentation":"After fully reading the response body (`resp.bytes()`), the actual byte count is checked against `WECHAT_MEDIA_MAX_BYTES` (100 MiB). This bail fires when the downloaded size exceeds the limit even though the `Content-Length` pre-check did not catch it — i.e. the header was absent (chunked transfer) or understated the real size. It is the enforcement backstop for error 335.","triggerScenarios":"A 2xx attachment download served with `Transfer-Encoding: chunked` and no `Content-Length`, or with a lying/smaller Content-Length, whose actual body exceeds 100 MiB. Reached via `send` with an oversized `https://` attachment target; also possible when a proxy re-chunks an upstream response.","commonSituations":"Dynamic endpoints that stream generated archives/videos without knowing the size ahead; misconfigured servers sending wrong Content-Length; proxies (some CDNs, ALBs) stripping Content-Length and switching to chunked; compressed-at-origin responses that decompress larger on the wire path. Same real-world content as 335: recordings, exports, datasets over 100 MiB.","solutions":["Same as the Content-Length case: produce a file under 100 MiB (compress/trim) or share an external link in the message body instead of attaching.","Check with `curl -sI <url>` (and `curl -s <url> | wc -c`) whether the header is missing or wrong, and fix the hosting to declare accurate sizes.","If you control the server, set an accurate Content-Length so the early check (error 335) aborts before the full download instead of after."],"exampleFix":"// before: attach an oversized dynamically-generated file\nlet target = format!(\"https://exports.example.com/report/{id}?format=zip\"); // >100 MiB, no Content-Length\nchannel.send(msg_with_attachment(&target)).await?; // downloads all bytes, then bails\n\n// after: probe size cheaply first, then choose link vs attachment\nlet size = probe_size(&target).await?; // HEAD, or stream-and-count early bytes\nif size > 100 * 1024 * 1024 {\n    channel.send(msg_with_text(&format!(\"Report too large, download: {target}\"))).await?;\n} else {\n    channel.send(msg_with_attachment(&target)).await?;\n}","handlingStrategy":"validation","validationCode":"// stream-count when no trustworthy Content-Length exists\nasync fn actual_size_at_most(client: &reqwest::Client, url: &str, max: u64) -> anyhow::Result<bool> {\n    use futures_util::StreamExt;\n    let resp = client.get(url).send().await?.error_for_status()?;\n    let mut stream = resp.bytes_stream();\n    let mut total = 0u64;\n    while let Some(chunk) = stream.next().await {\n        total += chunk?.len() as u64;\n        if total > max { return Ok(false); } // abort early\n    }\n    Ok(true)\n}","typeGuard":"fn within_wechat_limit(len: u64) -> bool { len <= 100 * 1024 * 1024 }","tryCatchPattern":"match channel.send(msg_with_attachment(&url)).await {\n    Err(err) if err.to_string().contains(\"exceeds 100 MB limit\") => {\n        let share = compress_or_rehost(&url).await?;\n        channel.send(msg_with_text(&format!(\"Large file: {share}\"))).await?;\n    }\n    other => other?,\n}","preventionTips":["Do not trust Content-Length alone; when the header is absent, stream-abort once the running total passes 100 MiB.","Prefer origins that declare accurate Content-Length so the cheap pre-check (error 335) fires instead of a full wasted download.","Generate attachment files/exports with a hard size budget at creation time."],"tags":["attachment","download","size-limit","wechat","chunked-transfer"],"backgroundTag":"download-size-limit-exceeded","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}