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

response body exceeds {max_bytes}-byte limit

Error message

response body exceeds {max_bytes}-byte limit

What it means

The streaming half of read_response_body_limited: when there is no usable Content-Length (Transfer-Encoding: chunked), the function accumulates chunks and checks the running total against max_bytes, bailing as soon as one more chunk would cross the cap. This bounds memory even when the server streams without declaring a size.

Source

Thrown at crates/zeroclaw-channels/src/util.rs:65

    max_bytes: u64,
) -> anyhow::Result<Vec<u8>> {
    if let Some(content_length) = response.content_length()
        && content_length > max_bytes
    {
        anyhow::bail!(
            "response body content length {content_length} exceeds {max_bytes}-byte limit"
        );
    }

    let mut body = Vec::new();

    while let Some(chunk) = response.chunk().await? {
        let chunk_len = u64::try_from(chunk.len()).unwrap_or(u64::MAX);
        let next_len = u64::try_from(body.len())
            .unwrap_or(u64::MAX)
            .saturating_add(chunk_len);
        if next_len > max_bytes {
            anyhow::bail!("response body exceeds {max_bytes}-byte limit");
        }
        body.extend_from_slice(&chunk);
    }

    Ok(body)
}

#[cfg(all(test, any(feature = "channel-mattermost", feature = "channel-qq")))]
pub(crate) async fn spawn_raw_http_response(
    raw_response: Vec<u8>,
    hold_open: bool,
) -> (String, tokio::task::JoinHandle<()>) {
    use tokio::io::{AsyncReadExt, AsyncWriteExt};

    let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
    let address = listener.local_addr().unwrap();
    let server = zeroclaw_spawn::spawn!(async move {
        let (mut socket, _) = listener.accept().await.unwrap();

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Treat it as a hard protocol bound: do not retry the same request expecting a different size.
  2. Move oversized transfers to a dedicated path with an explicit, purpose-sized cap.
  3. Check whether an intermediary proxy re-chunked a response that legitimately fits, and size the cap above real payloads plus encoding overhead.
Defensive patterns

Strategy: try-catch

Try / catch

match read_response_body_limited(response, max_bytes).await {
    Ok(body) => Ok(body),
    Err(err) if err.to_string().contains("response body exceeds") => {
        // Chunked/streaming body crossed the cap mid-stream: abort, do not
        // resume or retry with the same cap.
        Err(err.context("streamed body over cap — move oversized transfers elsewhere"))
    }
    Err(err) => Err(err),
}

Prevention

When it happens

Trigger: A chunked response whose cumulative body exceeds max_bytes mid-stream — the function aborts partway through rather than buffering the whole thing. The unit test drives exactly this: chunks arriving until the limit is exceeded triggers the bail before the response ends.

Common situations: Proxy servers (nginx, load balancers) that re-encode responses as chunked, removing Content-Length, so only this streaming check protects the process. Servers streaming unbounded or corrupt payloads hit it first.

Related errors


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