use std::io::Read;
use std::net::TcpStream;
use std::time::Duration;
const READ_TIMEOUT: Duration = Duration::from_secs(5);
pub fn read_request(stream: &mut TcpStream) -> String {
stream.set_nonblocking(false).expect("set blocking reads");
stream
.set_read_timeout(Some(READ_TIMEOUT))
.expect("set read timeout");
read_message(stream)
}
pub fn read_message(reader: &mut impl Read) -> String {
const HEAD_END: &[u8] = b"\r\n\r\n";
let mut data = Vec::new();
let mut chunk = [0_u8; 4096];
let head_len = loop {
if let Some(pos) = data.windows(HEAD_END.len()).position(|w| w == HEAD_END) {
break pos + HEAD_END.len();
}
read_chunk(reader, &mut chunk, &mut data, "head");
};
let total_len = head_len + content_length(&data[..head_len]);
while data.len() < total_len {
read_chunk(reader, &mut chunk, &mut data, "body");
}
data.truncate(total_len);
String::from_utf8_lossy(&data).into_owned()
}
fn read_chunk(reader: &mut impl Read, chunk: &mut [u8], data: &mut Vec<u8>, part: &str) {
let read = reader
.read(chunk)
.unwrap_or_else(|err| panic!("read request {part}: {err}"));
assert_ne!(
read, 0,
"the client closed the connection before the request {part} ended"
);
data.extend_from_slice(&chunk[..read]);
}
fn content_length(head: &[u8]) -> usize {
String::from_utf8_lossy(head)
.lines()
.find_map(|line| {
let (name, value) = line.split_once(':')?;
name.trim()
.eq_ignore_ascii_case("content-length")
.then(|| value.trim().parse().expect("numeric content-length"))
})
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use std::collections::VecDeque;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;
use std::time::{Duration, Instant};
use super::{READ_TIMEOUT, read_message, read_request};
const HEAD: &str = "POST /v1/review HTTP/1.1\r\nHost: localhost\r\nContent-Length: 11\r\n\r\n";
const BODY: &str = "{\"ok\":true}";
const FRAGMENT_DELAY: Duration = Duration::from_millis(100);
const ACCEPT_POLL: Duration = Duration::from_millis(10);
struct Chunks(VecDeque<Vec<u8>>);
impl Read for Chunks {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let Some(chunk) = self.0.pop_front() else {
return Ok(0);
};
buf[..chunk.len()].copy_from_slice(&chunk);
Ok(chunk.len())
}
}
fn chunks(parts: &[&str]) -> Chunks {
Chunks(parts.iter().map(|part| part.as_bytes().to_vec()).collect())
}
#[test]
fn reads_a_body_that_arrives_after_the_head() {
let request = read_message(&mut chunks(&[HEAD, BODY]));
assert_eq!(request, format!("{HEAD}{BODY}"));
}
#[test]
fn reads_a_request_split_at_any_byte() {
let full = format!("{HEAD}{BODY}");
for split in 1..full.len() {
let parts: [&str; 2] = full.split_at(split).into();
let request = read_message(&mut chunks(&parts));
assert_eq!(request, full, "split at byte {split}");
}
}
#[test]
fn stops_after_the_head_when_there_is_no_body() {
let head = "GET /v1/status HTTP/1.1\r\nHost: localhost\r\n\r\n";
let mut reader = chunks(&[head, "not part of this request"]);
assert_eq!(read_message(&mut reader), head);
}
#[test]
fn matches_the_content_length_header_in_any_case() {
let head = "POST / HTTP/1.1\r\ncontent-length: 2\r\n\r\n";
assert_eq!(
read_message(&mut chunks(&[head, "{}"])),
format!("{head}{{}}")
);
}
fn delayed_request_after_acceptance(explicit_nonblocking: bool) -> String {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind stub");
listener
.set_nonblocking(true)
.expect("nonblocking listener");
let mut client =
TcpStream::connect(listener.local_addr().expect("stub addr")).expect("connect");
client.set_nodelay(true).expect("no delay");
let deadline = Instant::now() + READ_TIMEOUT;
let mut stream = loop {
match listener.accept() {
Ok((stream, _)) => break stream,
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
assert!(Instant::now() < deadline, "client was not accepted");
thread::sleep(ACCEPT_POLL);
}
Err(error) => panic!("accept connected client: {error}"),
}
};
if explicit_nonblocking {
stream.set_nonblocking(true).expect("nonblocking stream");
}
thread::scope(|scope| {
scope.spawn(move || {
thread::sleep(FRAGMENT_DELAY);
client.write_all(HEAD.as_bytes()).expect("write head");
thread::sleep(FRAGMENT_DELAY);
client.write_all(BODY.as_bytes()).expect("write body");
});
read_request(&mut stream)
})
}
#[test]
fn reads_a_delayed_request_from_a_nonblocking_listener() {
assert_eq!(
delayed_request_after_acceptance(false),
format!("{HEAD}{BODY}")
);
}
#[test]
fn reads_a_delayed_request_from_a_nonblocking_stream() {
assert_eq!(
delayed_request_after_acceptance(true),
format!("{HEAD}{BODY}")
);
}
#[test]
fn reads_a_body_sent_in_a_second_write_over_tcp() {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind stub");
let addr = listener.local_addr().expect("stub addr");
let client = thread::spawn(move || {
let mut stream = TcpStream::connect(addr).expect("connect");
stream.set_nodelay(true).expect("no delay");
stream.write_all(HEAD.as_bytes()).expect("write head");
stream.flush().expect("flush head");
thread::sleep(FRAGMENT_DELAY);
stream.write_all(BODY.as_bytes()).expect("write body");
stream
});
let (mut stream, _) = listener.accept().expect("accept");
let request = read_request(&mut stream);
drop(client.join().expect("client thread"));
assert_eq!(request, format!("{HEAD}{BODY}"));
}
}