Back to blog

Building a simple http server in rust

serve some html to the browser

so my idea is pretty simple, lets try to build an http server

first we need a listener that attaches to a socket. i'm using rust which obfuscate the bare minimums. you can get a tcpstream to listen to by binding a host and port

let host = "127.0.0.1";
let port = "8080";
let listener = TcpListener::bind(format!("{host}:{port}")).unwrap();

Next we need to be able to "hear" what the thing is receiving. So to do it forever we can execute .incoming() to pick up on the incoming streams being attached to the socket

listener.incoming().for_each(|stream| match stream {
  Ok(mut stream) => {
    handle_connection(&stream).unwrap();
    handle_response(&mut stream);
  },
  Err(e) => println!("> couldn't get client: {e:?}"),
});

Reading the stream is easier passing it to a buffer to be able to read it line by line right now we only care about the first line of the response because it has the method, route it's requesting & the protocol. I will use that later to decide what to do with the request.

fn handle_connection(stream: &TcpStream) -> std::io::Result<()> {
    let mut reader = BufReader::new(stream);
    let mut buf = String::new();
 
    println!("> receiving query...");
    reader.read_line(&mut buf)?;
 
    let parts = buf.split(" ").collect::<Vec<&str>>();
    let method = parts[0];
    let segment = parts[1];
    println!("> method: {method}");
    println!("> segment: {segment}");
    println!("> end of message");
 
    Ok(())
}

We will leave routing for tomorrow for now I will only serve local index.html for that I'm going to set path that strips the absolute root first. We can verify that it is a file and they client is making a GET request because I felt like it. Then we check the file, if there is no issues we output the html. We will need to check for the file type in the future for time being it is being ignored. The response need at least the payload size, it's content type and the header should show the estatus with the protocol. At least that's what I've found out so far. Then we just write and flush to the client.

fn handle_response(stream: &mut TcpStream, req: Request) {
    let path = Path::new(&req.segment).strip_prefix("/").unwrap();
    println!("> loading query: {:?}", path);
    if path.is_file() && req.method == "GET" {
        match fs::read_to_string(path) {
            Ok(content) => {
                let content_type = "text/html";
                let content_len = content.len();
                let response = format!(
                    "HTTP/1.1 200 OK\n\
                Content-Type: {content_type}\n\
                Content-Length: {content_len}\n\n\
                {content}"
                );
 
                stream.write(response.as_bytes()).unwrap();
                stream.flush().unwrap();
            }
            Err(e) => {
                let content = "Not found";
                let content_type = "text/plain";
                let content_len = content.len();
                let response = format!(
                    "HTTP/1.1 404 ERROR\n\
                Content-Type: {content_type}\n\
                Content-Length: {content_len}\n\n\
                {content}"
                );
 
                stream.write(response.as_bytes()).unwrap();
                stream.flush().unwrap();
                println!("> {e}")
            }
        }
    }
}