Skip to main content

codetether_agent/provider/
body_cap.rs

1//! Bounded HTTP body readers — cap peak memory for provider `/models`
2//! (and similar) calls so that a runaway multi-megabyte response cannot
3//! OOM the process during startup/registration.
4
5use anyhow::{Context, Result, bail};
6use futures::StreamExt;
7use reqwest::Response;
8
9/// Default cap for provider catalog / metadata responses. Large enough
10/// for realistic `/models` payloads (OpenRouter currently ~0.5 MiB),
11/// small enough that an unbounded or hostile response cannot blow the
12/// process stack.
13pub const PROVIDER_METADATA_BODY_CAP: usize = 8 * 1024 * 1024; // 8 MiB
14
15/// Read an HTTP response body into memory, failing if it exceeds
16/// `max_bytes`. Streams chunks so the peak allocation is bounded by
17/// `max_bytes + chunk_size`.
18pub async fn read_body_capped(resp: Response, max_bytes: usize) -> Result<Vec<u8>> {
19    if let Some(len) = resp.content_length()
20        && len as usize > max_bytes
21    {
22        bail!(
23            "response body {} B exceeds cap {} B (Content-Length)",
24            len,
25            max_bytes,
26        );
27    }
28    let mut buf: Vec<u8> = Vec::with_capacity(16 * 1024);
29    let mut stream = resp.bytes_stream();
30    while let Some(chunk) = stream.next().await {
31        let chunk = chunk.context("read response chunk")?;
32        if buf.len().saturating_add(chunk.len()) > max_bytes {
33            bail!(
34                "response body exceeded cap {} B after {} B",
35                max_bytes,
36                buf.len(),
37            );
38        }
39        buf.extend_from_slice(&chunk);
40    }
41    Ok(buf)
42}
43
44/// Convenience: fetch JSON with a hard body cap. Returns `T` on success,
45/// or an error if the cap is exceeded / parsing fails.
46pub async fn json_capped<T: serde::de::DeserializeOwned>(
47    resp: Response,
48    max_bytes: usize,
49) -> Result<T> {
50    let bytes = read_body_capped(resp, max_bytes).await?;
51    serde_json::from_slice::<T>(&bytes).context("decode capped JSON body")
52}