zeroclaw-labs/zeroclaw · error · anyhow::Error

attachment exceeds {} MB limit

Error message

attachment exceeds {} MB limit

What it means

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.

Source

Thrown at crates/zeroclaw-channels/src/wechat.rs:1206

        if let Some(len) = resp.content_length()
            && len > WECHAT_MEDIA_MAX_BYTES
        {
            anyhow::bail!(
                "attachment Content-Length ({len} bytes) exceeds {} MB limit",
                WECHAT_MEDIA_MAX_BYTES / (1024 * 1024)
            );
        }

        let content_type = resp
            .headers()
            .get(reqwest::header::CONTENT_TYPE)
            .and_then(|value| value.to_str().ok())
            .map(str::to_string);
        let bytes = resp.bytes().await?.to_vec();

        if bytes.len() as u64 > WECHAT_MEDIA_MAX_BYTES {
            anyhow::bail!(
                "attachment exceeds {} MB limit",
                WECHAT_MEDIA_MAX_BYTES / (1024 * 1024)
            );
        }

        Ok(WeChatMediaPayload {
            file_name: self.remote_file_name(url, content_type.as_deref(), kind),
            bytes,
        })
    }

    async fn load_attachment_payload(
        &self,
        attachment: &WeChatAttachment,
    ) -> anyhow::Result<WeChatMediaPayload> {
        let target = attachment.target.trim();
        if is_remote_url(target) {
            return self

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. 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.
  2. 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.
  3. If you control the server, set an accurate Content-Length so the early check (error 335) aborts before the full download instead of after.

Example fix

// before: attach an oversized dynamically-generated file
let target = format!("https://exports.example.com/report/{id}?format=zip"); // >100 MiB, no Content-Length
channel.send(msg_with_attachment(&target)).await?; // downloads all bytes, then bails

// after: probe size cheaply first, then choose link vs attachment
let size = probe_size(&target).await?; // HEAD, or stream-and-count early bytes
if size > 100 * 1024 * 1024 {
    channel.send(msg_with_text(&format!("Report too large, download: {target}"))).await?;
} else {
    channel.send(msg_with_attachment(&target)).await?;
}
Defensive patterns

Strategy: validation

Validate before calling

// stream-count when no trustworthy Content-Length exists
async fn actual_size_at_most(client: &reqwest::Client, url: &str, max: u64) -> anyhow::Result<bool> {
    use futures_util::StreamExt;
    let resp = client.get(url).send().await?.error_for_status()?;
    let mut stream = resp.bytes_stream();
    let mut total = 0u64;
    while let Some(chunk) = stream.next().await {
        total += chunk?.len() as u64;
        if total > max { return Ok(false); } // abort early
    }
    Ok(true)
}

Type guard

fn within_wechat_limit(len: u64) -> bool { len <= 100 * 1024 * 1024 }

Try / catch

match channel.send(msg_with_attachment(&url)).await {
    Err(err) if err.to_string().contains("exceeds 100 MB limit") => {
        let share = compress_or_rehost(&url).await?;
        channel.send(msg_with_text(&format!("Large file: {share}"))).await?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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