use serde_json::Value;
use std::io::{self, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
pub fn spawn_http_responses(
responses: Vec<(u16, Value)>,
) -> (String, JoinHandle<io::Result<Vec<String>>>) {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server");
listener
.set_nonblocking(true)
.expect("set test server nonblocking");
let addr = listener.local_addr().expect("local addr");
let handle = thread::spawn(move || {
let mut requests = Vec::new();
for (status, body) in responses {
let mut stream = accept_with_timeout(&listener, Duration::from_secs(5))?;
stream.set_nonblocking(false)?;
requests.push(read_http_request(&mut stream)?);
let body = body.to_string();
write!(
stream,
"HTTP/1.1 {status} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
reason_phrase(status),
body.len()
)?;
}
Ok(requests)
});
(format!("http://{addr}"), handle)
}
fn accept_with_timeout(listener: &TcpListener, timeout: Duration) -> io::Result<TcpStream> {
let deadline = Instant::now() + timeout;
loop {
match listener.accept() {
Ok((stream, _)) => return Ok(stream),
Err(err) if err.kind() == io::ErrorKind::WouldBlock => {
if Instant::now() >= deadline {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
"timed out waiting for test HTTP request",
));
}
thread::sleep(Duration::from_millis(10));
}
Err(err) => return Err(err),
}
}
}
fn read_http_request(stream: &mut TcpStream) -> io::Result<String> {
stream.set_read_timeout(Some(Duration::from_secs(2)))?;
let mut request = Vec::new();
let mut buffer = [0; 4096];
let mut expected_len = None;
loop {
match stream.read(&mut buffer) {
Ok(0) => {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"connection closed before complete test HTTP request",
));
}
Ok(n) => request.extend_from_slice(&buffer[..n]),
Err(err)
if matches!(
err.kind(),
io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
) =>
{
return Err(io::Error::new(
io::ErrorKind::TimedOut,
"timed out reading complete test HTTP request",
));
}
Err(err) => return Err(err),
}
if expected_len.is_none()
&& let Some(header_end) = request.windows(4).position(|window| window == b"\r\n\r\n")
{
let headers = String::from_utf8_lossy(&request[..header_end]);
let content_len = parse_content_length(&headers)?;
expected_len = Some(header_end + 4 + content_len);
}
if let Some(expected_len) = expected_len
&& request.len() >= expected_len
{
return Ok(String::from_utf8_lossy(&request).into_owned());
}
}
}
fn parse_content_length(headers: &str) -> io::Result<usize> {
for line in headers.lines() {
let Some((name, value)) = line.split_once(':') else {
continue;
};
if name.eq_ignore_ascii_case("content-length") {
return value.trim().parse::<usize>().map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("invalid Content-Length: {err}"),
)
});
}
}
Ok(0)
}
fn reason_phrase(status: u16) -> &'static str {
match status {
100 => "Continue",
101 => "Switching Protocols",
200 => "OK",
201 => "Created",
202 => "Accepted",
204 => "No Content",
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
409 => "Conflict",
415 => "Unsupported Media Type",
422 => "Unprocessable Entity",
429 => "Too Many Requests",
500 => "Internal Server Error",
502 => "Bad Gateway",
503 => "Service Unavailable",
504 => "Gateway Timeout",
_ => "Unknown",
}
}