use crate::{Error, PubkyHttpClient, errors::RequestError};
use reqwest::{Response, StatusCode};
use std::{fmt::Write, time::Duration};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::{TcpListener, TcpStream},
};
fn client(limit: Option<usize>) -> PubkyHttpClient {
let mut builder = PubkyHttpClient::builder();
builder.isolated_pkarr_test();
if let Some(limit) = limit {
builder.max_error_body_bytes(limit);
}
builder.build().unwrap()
}
pub(crate) async fn serve_response(
status: u16,
framing: &str,
body: &[u8],
) -> (Response, TcpStream) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let url = format!("http://{}", listener.local_addr().unwrap());
let client = reqwest::Client::new();
let send = client.get(url).send();
let serve = async {
let (mut socket, _) = listener.accept().await.unwrap();
let mut request = Vec::new();
loop {
let byte = socket.read_u8().await.unwrap();
request.push(byte);
if request.ends_with(b"\r\n\r\n") {
break;
}
assert!(request.len() < 16 * 1024);
}
socket
.write_all(format!("HTTP/1.1 {status} Test\r\n{framing}\r\n").as_bytes())
.await
.unwrap();
socket.write_all(body).await.unwrap();
socket
};
let (response, socket) = tokio::join!(send, serve);
(response.unwrap(), socket)
}
async fn server_error(response: Response, client: PubkyHttpClient) -> (StatusCode, String) {
let error = tokio::time::timeout(Duration::from_secs(2), client.check_http_status(response))
.await
.expect("status handling must not wait for the rest of an oversized body")
.unwrap_err();
let Error::Request(RequestError::Server { status, message }) = error else {
panic!("unexpected error: {error:?}");
};
(status, message)
}
#[tokio::test]
async fn oversized_error_stops_before_eof() {
let body = format!("1001\r\n{}\r\n", "x".repeat(4097));
let (response, _socket) =
serve_response(500, "Transfer-Encoding: chunked\r\n", body.as_bytes()).await;
let (status, message) = server_error(response, client(None)).await;
assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(
message,
format!(
"{}\n[response body truncated at 4096 bytes]",
"x".repeat(4096)
)
);
}
#[tokio::test]
async fn error_body_limits_follow_client_configuration() {
for setting in [None, Some(16), Some(8192)] {
let client = client(setting);
let limit = setting.unwrap_or(4096);
for size in [0, 3, limit - 1, limit, limit + 1] {
let body = vec![b'x'; size];
let (response, _socket) =
serve_response(429, &format!("Content-Length: {size}\r\n"), &body).await;
let (status, message) = server_error(response, client.clone()).await;
assert_eq!(status, StatusCode::TOO_MANY_REQUESTS);
let mut expected = "x".repeat(size.min(limit));
if size > limit {
write!(expected, "\n[response body truncated at {limit} bytes]").unwrap();
}
assert_eq!(message, expected, "limit {limit}, body size {size}");
}
}
}
#[tokio::test]
async fn clients_keep_independent_limits_when_cloned() {
let small = client(Some(3));
let larger = client(Some(5));
for (client, limit) in [(small.clone(), 3), (larger, 5), (small, 3)] {
let (response, _socket) =
serve_response(500, "Content-Length: 10\r\n", b"xxxxxxxxxx").await;
let (_, message) = server_error(response, client).await;
assert_eq!(
message,
format!(
"{}\n[response body truncated at {limit} bytes]",
"x".repeat(limit)
)
);
}
}
#[tokio::test]
async fn zero_limit_does_not_wait_for_error_bodies() {
for (status, reason) in [
(404, "Not Found"),
(410, "Gone"),
(500, "Internal Server Error"),
(599, "Unknown Error"),
] {
let (response, _socket) =
serve_response(status, "Content-Length: 1000000000\r\n", b"").await;
let (actual, message) = server_error(response, client(Some(0))).await;
assert_eq!(actual.as_u16(), status);
assert_eq!(message, reason);
}
}
#[tokio::test]
async fn large_limit_does_not_require_a_large_allocation() {
let (response, _socket) = serve_response(500, "Content-Length: 5\r\n", b"short").await;
let (_, message) = server_error(response, client(Some(usize::MAX))).await;
assert_eq!(message, "short");
}
#[tokio::test]
async fn error_limit_is_independent_of_framing_and_chunk_boundaries() {
for framing in ["", "Content-Length: 1000000000000\r\n"] {
let (response, _socket) = serve_response(503, framing, &vec![b'x'; 4097]).await;
let (_, message) = server_error(response, client(None)).await;
assert_eq!(
message,
format!(
"{}\n[response body truncated at 4096 bytes]",
"x".repeat(4096)
)
);
}
let (response, mut socket) = serve_response(500, "Transfer-Encoding: chunked\r\n", b"").await;
let read = tokio::spawn(server_error(response, client(None)));
for _ in 0..4096 {
socket.write_all(b"1\r\nx\r\n").await.unwrap();
}
socket.write_all(b"1\r\ny\r\n").await.unwrap();
let (_, message) = read.await.unwrap();
assert_eq!(
message,
format!(
"{}\n[response body truncated at 4096 bytes]",
"x".repeat(4096)
)
);
}
#[tokio::test]
async fn checked_success_does_not_consume_the_body() {
let (response, mut socket) = serve_response(200, "Content-Length: 5\r\n", b"").await;
let response = tokio::time::timeout(
Duration::from_secs(2),
client(Some(0)).check_http_status(response),
)
.await
.unwrap()
.unwrap();
socket.write_all(b"hello").await.unwrap();
assert_eq!(response.text().await.unwrap(), "hello");
}
#[tokio::test]
async fn error_text_decoding_and_read_failure() {
for (body, expected) in [
(b"\xef\xbb\xbfhello".as_slice(), "hello"),
(b"\xef\xbb\xbf", ""),
("hello\u{FEFF}".as_bytes(), "hello\u{FEFF}"),
(b"bad\xfftext", "bad\u{fffd}text"),
("caf\u{e9}".as_bytes(), "caf\u{e9}"),
] {
let (response, _socket) =
serve_response(400, &format!("Content-Length: {}\r\n", body.len()), body).await;
let (_, message) = server_error(response, client(None)).await;
assert_eq!(message, expected);
}
let mut body = vec![b'x'; 4095];
body.extend_from_slice("\u{20ac}".as_bytes());
let (response, _socket) =
serve_response(400, &format!("Content-Length: {}\r\n", body.len()), &body).await;
let (_, message) = server_error(response, client(None)).await;
assert_eq!(
message,
format!(
"{}\u{fffd}\n[response body truncated at 4096 bytes]",
"x".repeat(4095)
)
);
for (status, fallback) in [(500, "Internal Server Error"), (599, "Unknown Error")] {
let (response, socket) =
serve_response(status, "Content-Length: 100\r\n", b"partial").await;
drop(socket);
let (actual, message) = server_error(response, client(None)).await;
assert_eq!(actual.as_u16(), status);
assert_eq!(message, fallback);
}
}
#[tokio::test]
async fn small_head_does_not_protect_against_oversized_get_errors() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let url = format!("http://{}", listener.local_addr().unwrap());
let peer = tokio::spawn(async move {
for method in ["HEAD", "GET"] {
let (mut socket, _) = listener.accept().await.unwrap();
let mut request = Vec::new();
while !request.ends_with(b"\r\n\r\n") {
request.push(socket.read_u8().await.unwrap());
assert!(request.len() < 16 * 1024);
}
assert!(request.starts_with(method.as_bytes()));
if method == "HEAD" {
socket
.write_all(
b"HTTP/1.1 200 OK\r\nContent-Length: 16\r\nConnection: close\r\n\r\n",
)
.await
.unwrap();
} else {
socket.write_all(format!("HTTP/1.1 500 Error\r\nTransfer-Encoding: chunked\r\n\r\n1001\r\n{}\r\n", "x".repeat(4097)).as_bytes()).await.unwrap();
return socket; }
}
unreachable!()
});
let http = reqwest::Client::new();
let head = http.head(&url).send().await.unwrap();
assert_eq!(head.headers()["content-length"], "16");
let response = http.get(&url).send().await.unwrap();
let _socket = peer.await.unwrap();
let (status, message) = server_error(response, client(None)).await;
assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(
message,
format!(
"{}\n[response body truncated at 4096 bytes]",
"x".repeat(4096)
)
);
}