pub(crate) fn truncate_str(s: &str, max_bytes: usize) -> String {
s[..s.floor_char_boundary(max_bytes)].to_owned()
}
pub(crate) const MAX_ERROR_BODY_DOWNLOAD_BYTES: usize = 64 * 1024;
pub(crate) async fn read_body_capped(
mut response: reqwest::Response,
cap: usize,
) -> Result<String, reqwest::Error> {
let mut buf: Vec<u8> = Vec::new();
while buf.len() < cap {
let Some(chunk) = response.chunk().await? else {
break;
};
let remaining = cap - buf.len();
if chunk.len() <= remaining {
buf.extend_from_slice(&chunk);
} else {
buf.extend_from_slice(&chunk[..remaining]);
break;
}
}
Ok(String::from_utf8(buf)
.unwrap_or_else(|error| String::from_utf8_lossy(error.as_bytes()).into_owned()))
}
#[cfg(test)]
mod truncate_str_tests {
use super::*;
#[test]
fn shorter_than_the_cap_is_returned_whole() {
assert_eq!(truncate_str("abc", 512), "abc");
}
#[test]
fn longer_than_the_cap_is_cut_at_the_exact_byte() {
assert_eq!(truncate_str("0123456789", 4), "0123");
}
#[test]
fn a_cap_splitting_a_multi_byte_char_rounds_down_instead_of_panicking() {
assert_eq!(truncate_str("€", 2), "");
assert_eq!(truncate_str("a€", 3), "a");
assert_eq!(truncate_str("a€", 4), "a€");
}
#[test]
fn a_cap_beyond_the_string_is_not_out_of_bounds() {
assert_eq!(truncate_str("abc", usize::MAX), "abc");
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
use super::*;
use wiremock::matchers::method;
use wiremock::{Mock, MockServer, ResponseTemplate};
async fn read_capped(body: impl Into<String>, cap: usize) -> String {
let server = MockServer::start().await;
Mock::given(method("GET"))
.respond_with(ResponseTemplate::new(500).set_body_string(body.into()))
.mount(&server)
.await;
let response = reqwest::Client::new()
.get(server.uri())
.send()
.await
.unwrap();
read_body_capped(response, cap).await.unwrap()
}
#[tokio::test]
async fn body_under_cap_is_returned_whole() {
assert_eq!(
read_capped("{\"error\":\"not found\"}", 1024).await,
"{\"error\":\"not found\"}"
);
}
#[tokio::test]
async fn body_over_cap_is_truncated_at_the_exact_byte() {
assert_eq!(read_capped("0123456789", 4).await, "0123");
}
#[tokio::test]
async fn cap_landing_mid_character_decodes_lossily_instead_of_panicking() {
assert_eq!(read_capped("é", 1).await, "\u{FFFD}");
}
#[tokio::test]
async fn empty_body_is_read_as_empty_rather_than_erroring() {
assert_eq!(read_capped("", 1024).await, "");
}
#[tokio::test]
async fn pathological_body_is_bounded_to_the_shared_download_cap() {
let oversized = "a".repeat(MAX_ERROR_BODY_DOWNLOAD_BYTES * 4);
let read = read_capped(oversized, MAX_ERROR_BODY_DOWNLOAD_BYTES).await;
assert_eq!(read.len(), MAX_ERROR_BODY_DOWNLOAD_BYTES);
}
}