use std::time::Duration;
pub(crate) const MAX_ERROR_BODY: usize = 64 * 1024;
pub(crate) const MAX_JSON_BODY: usize = 4 * 1024 * 1024;
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const REQUEST_TIMEOUT: Duration = Duration::from_secs(120);
pub(crate) fn bounded_client() -> reqwest::Client {
reqwest::Client::builder()
.connect_timeout(CONNECT_TIMEOUT)
.timeout(REQUEST_TIMEOUT)
.build()
.unwrap_or_else(|_| reqwest::Client::new())
}
pub(crate) async fn read_body_capped(response: reqwest::Response, cap: usize) -> String {
let mut response = response;
let mut buffer: Vec<u8> = Vec::new();
loop {
let remaining = cap.saturating_sub(buffer.len());
if remaining == 0 {
break;
}
match response.chunk().await {
Ok(Some(chunk)) => {
let take = remaining.min(chunk.len());
buffer.extend_from_slice(&chunk[..take]);
if take < chunk.len() {
break;
}
}
Ok(None) | Err(_) => break,
}
}
String::from_utf8_lossy(&buffer).into_owned()
}
pub(crate) async fn read_bytes_capped(
response: reqwest::Response,
cap: usize,
) -> Result<Vec<u8>, reqwest::Error> {
let mut response = response;
let mut buffer: Vec<u8> = Vec::new();
while buffer.len() < cap {
match response.chunk().await? {
Some(chunk) => {
let remaining = cap - buffer.len();
let take = remaining.min(chunk.len());
buffer.extend_from_slice(&chunk[..take]);
if take < chunk.len() {
break;
}
}
None => break,
}
}
Ok(buffer)
}
#[cfg(test)]
mod tests {
use std::io::{Read as _, Write as _};
use std::net::TcpListener;
use std::thread::{self, JoinHandle};
use super::*;
fn serve_body(body: Vec<u8>) -> (String, JoinHandle<()>) {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr");
let handle = thread::spawn(move || {
let Ok((mut stream, _)) = listener.accept() else {
return;
};
let mut buf = [0_u8; 1024];
let _ = stream.read(&mut buf);
let head = format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
);
let _ = stream.write_all(head.as_bytes());
let _ = stream.write_all(&body);
let _ = stream.flush();
});
(format!("http://{addr}"), handle)
}
async fn get(url: &str) -> reqwest::Response {
reqwest::Client::new().get(url).send().await.expect("send")
}
#[tokio::test]
async fn read_bytes_capped_keeps_exact_limit_and_truncates_over_limit() {
let (base, handle) = serve_body(vec![b'a'; 10]);
let bytes = read_bytes_capped(get(&base).await, 10).await.expect("read");
assert_eq!(bytes.len(), 10);
handle.join().expect("join");
let (base, handle) = serve_body(vec![b'a'; 11]);
let bytes = read_bytes_capped(get(&base).await, 10).await.expect("read");
assert_eq!(bytes.len(), 10, "over-limit body truncated to cap");
handle.join().expect("join");
let (base, handle) = serve_body(vec![b'a'; 4]);
let bytes = read_bytes_capped(get(&base).await, 10).await.expect("read");
assert_eq!(bytes.len(), 4, "under-limit body returned whole");
handle.join().expect("join");
}
#[tokio::test]
async fn read_body_capped_keeps_exact_limit_and_truncates_over_limit() {
let (base, handle) = serve_body(vec![b'z'; 10]);
let body = read_body_capped(get(&base).await, 10).await;
assert_eq!(body.chars().count(), 10);
handle.join().expect("join");
let (base, handle) = serve_body(vec![b'z'; 11]);
let body = read_body_capped(get(&base).await, 10).await;
assert_eq!(body.chars().count(), 10, "over-limit error body truncated");
handle.join().expect("join");
}
}