use crate::safety::budget_exceeded;
use anyhow::Result;
use serde::de::DeserializeOwned;
pub const AI_BODY_MAX: usize = 32 * 1024 * 1024;
fn too_large(max: usize) -> anyhow::Error {
budget_exceeded(
"http_body",
format!("the response body is more than {max} bytes"),
"the endpoint sent a larger response than the shell accepts; check the endpoint or the request",
)
}
fn check_declared_length(declared: Option<u64>, max: usize) -> Result<()> {
match declared {
Some(n) if n > max as u64 => Err(too_large(max)),
_ => Ok(()),
}
}
fn push_chunk(buf: &mut Vec<u8>, chunk: &[u8], max: usize) -> Result<()> {
if buf.len().saturating_add(chunk.len()) > max {
return Err(too_large(max));
}
buf.extend_from_slice(chunk);
Ok(())
}
#[cfg(test)]
fn accumulate_capped<C, I>(chunks: I, max: usize) -> Result<Vec<u8>>
where
I: IntoIterator<Item = Result<C>>,
C: AsRef<[u8]>,
{
let mut buf = Vec::new();
for chunk in chunks {
let chunk = chunk?;
push_chunk(&mut buf, chunk.as_ref(), max)?;
}
Ok(buf)
}
pub async fn body_bytes_capped(mut resp: reqwest::Response, max: usize) -> Result<Vec<u8>> {
check_declared_length(resp.content_length(), max)?;
let mut buf = Vec::new();
while let Some(chunk) = resp.chunk().await? {
push_chunk(&mut buf, &chunk, max)?;
}
Ok(buf)
}
pub async fn body_text_capped(resp: reqwest::Response, max: usize) -> Result<String> {
let bytes = body_bytes_capped(resp, max).await?;
Ok(String::from_utf8_lossy(&bytes).into_owned())
}
pub async fn body_json_capped<T: DeserializeOwned>(
resp: reqwest::Response,
max: usize,
) -> Result<T> {
let bytes = body_bytes_capped(resp, max).await?;
serde_json::from_slice(&bytes).map_err(|e| anyhow::anyhow!("error decoding response body: {e}"))
}
pub fn body_bytes_capped_blocking(
mut resp: reqwest::blocking::Response,
max: usize,
) -> Result<Vec<u8>> {
use std::io::Read;
check_declared_length(resp.content_length(), max)?;
let mut buf = Vec::new();
Read::take(&mut resp, (max as u64).saturating_add(1))
.read_to_end(&mut buf)
.map_err(|e| anyhow::anyhow!("error reading response body: {e}"))?;
if buf.len() > max {
return Err(too_large(max));
}
Ok(buf)
}
pub fn body_text_capped_blocking(resp: reqwest::blocking::Response, max: usize) -> Result<String> {
let bytes = body_bytes_capped_blocking(resp, max)?;
Ok(String::from_utf8_lossy(&bytes).into_owned())
}
pub fn body_json_capped_blocking<T: DeserializeOwned>(
resp: reqwest::blocking::Response,
max: usize,
) -> Result<T> {
let bytes = body_bytes_capped_blocking(resp, max)?;
serde_json::from_slice(&bytes).map_err(|e| anyhow::anyhow!("error decoding response body: {e}"))
}
pub trait BlockingBodyExt {
fn text_capped(self) -> Result<String>;
fn json_capped<T: DeserializeOwned>(self) -> Result<T>;
}
impl BlockingBodyExt for reqwest::blocking::Response {
fn text_capped(self) -> Result<String> {
body_text_capped_blocking(self, AI_BODY_MAX)
}
fn json_capped<T: DeserializeOwned>(self) -> Result<T> {
body_json_capped_blocking(self, AI_BODY_MAX)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::safety::{ErrorCode, SafetyError};
fn code_of(err: &anyhow::Error) -> Option<ErrorCode> {
err.downcast_ref::<SafetyError>().map(|s| s.code)
}
#[test]
fn under_limit_is_read_whole() {
let chunks = vec![Ok::<_, anyhow::Error>(b"ab".to_vec()), Ok(b"cd".to_vec())];
assert_eq!(accumulate_capped(chunks, 4).unwrap(), b"abcd");
}
#[test]
fn exactly_at_limit_is_accepted() {
let chunks = vec![Ok::<_, anyhow::Error>(vec![7u8; 4])];
assert_eq!(accumulate_capped(chunks, 4).unwrap().len(), 4);
}
#[test]
fn over_limit_is_budget_exceeded() {
let chunks = vec![Ok::<_, anyhow::Error>(vec![0u8; 3]), Ok(vec![0u8; 2])];
let err = accumulate_capped(chunks, 4).unwrap_err();
assert_eq!(code_of(&err), Some(ErrorCode::BudgetExceeded));
}
#[test]
fn declared_length_over_limit_is_refused_before_reading() {
let err = check_declared_length(Some(5), 4).unwrap_err();
assert_eq!(code_of(&err), Some(ErrorCode::BudgetExceeded));
assert!(check_declared_length(Some(4), 4).is_ok());
assert!(check_declared_length(None, 4).is_ok());
}
#[test]
fn read_error_is_not_reported_as_budget() {
let chunks = vec![Err::<Vec<u8>, _>(anyhow::anyhow!("connection reset"))];
let err = accumulate_capped(chunks, 4).unwrap_err();
assert_eq!(code_of(&err), None);
}
}