Build Log
A chronological record of my suffering.
Main Branch
2026-01-27
HTTP 1.1, RFC 9110, RFC 9112
Layout of a HTTP Message.
HTTP Message Syntax
METHOD /resource-path PROTOCOL-VERSION\r\n
GET /cat-jokes HTTP/1.1\r\n
Created an enum to track HTTP Errors.
pub enum HttpError {
MissingRequestLine,
InvalidMethod(String),
InvalidPath(String),
InvalidVersion(String),
MalformedRequestLine,
MissingCRLF,
IoError(std::io::Error),
}
impl From<std::io::Error> for HttpError {
fn from(err: std::io::Error) -> Self {
HttpError::IoError(err)
}
}
Originally I just had, individual checks for each part.
let method = parts.next().ok_or(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid request line",
))?;
let request_target = parts.next().ok_or(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid request line",
))?;
let version = parts.next().ok_or(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid request line",
))?;
but thats pretty awful... I decided to use the thiserror crate.
use thiserror::Error;
#[derive(Error, Debug)]
pub enum HttpError {
#[error("missing request line")]
MissingRequestLine,
#[error("invalid HTTP method: {0}")]
InvalidMethod(String),
#[error("invalid request path: {0}")]
InvalidPath(String),
#[error("invalid HTTP version: {0}, expected HTTP/1.0 or HTTP/1.1")]
InvalidVersion(String),
#[error("malformed request line: expected 'METHOD PATH VERSION'")]
MalformedRequestLine,
#[error("missing CRLF line terminator")]
MissingCRLF,
#[error("missing required header: {0}")]
MissingHeader(String),
#[error("invalid header format: {0}")]
InvalidHeader(String),
#[error(transparent)]
IoError(#[from] std::io::Error),
}
We need to have the ability to read in a request in chunks and then parse it.
loop {
let bytes_read = reader.read(&mut read_buffer)?;
if bytes_read == 0 {
if request.state != ParseState::Complete {
return Err(HttpError::MissingRequestLine);
}
break;
}
let chunk = String::from_utf8_lossy(&read_buffer[..bytes_read]);
let complete = request.parse_chunk(&chunk, &mut buffer)?;
if complete {
break;
}
}
2026-01-24
Basic implementation of a reader loop that reads in 8 bytes at time from socket connection. For this I technically do not need to use mpsc::channel but in some of the examples I have seen (which happened to be in Go) it seems like a channel was implemented.
fn get_lines<R>(mut reader: R) -> mpsc::Receiver<String>
where
R: Read + Send + 'static,
{
let (sender, receiver) = mpsc::channel::<String>();
thread::spawn(move || {
let mut buf = [0; 8];
let mut current_line = String::new();
loop {
match reader.read(&mut buf) {
Ok(0) => break, Ok(n) => {
let line = String::from_utf8_lossy(&buf[0..n]).to_string();
current_line.push_str(&line);
let mut parts: Vec<String> = current_line.split('\n').map(String::from).collect();
current_line = parts.pop().unwrap_or_default();
for part in parts {
sender.send(part).unwrap();
}
}
Err(e) => {
eprint!("Error: {}", e);
break;
}
}
}
sender.send(current_line).unwrap();
});
receiver
}
And here is how it's used in the main loop:
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
match stream {
Ok(stream) => {
let receiver = get_lines(stream);
for line in receiver {
println!("{}", line);
}
}
Err(e) => {
eprint!("Error: {}", e);
}
}
}
}