# Build a web server with Rust and tokio - Part 0: a simple GET handler

# Build a web server with Rust and tokio - Part 0: the simplest possible GET handler

Welcome to this series of blog posts where we will be exploring how to build a web server from scratch using the Rust programming language. We will be taking a hands-on approach, maximizing our learning experience by using as few dependencies as possible and implementing as much logic as we can. This will enable us to understand the inner workings of a web server and the underlying protocols that it uses.

By the end of this tutorial, you will have a solid understanding of how to build a web server from scratch using Rust and the tokio library. So, let's dive in and get started on our journey!

In this first part, we'll be building a barebones web server that can only anwser GET requests with a static Not Found response. This will give us a good starting point to build upon in the following tutorial.

## Setting up our project

First, we need to create a new Rust project. We'll use the following crates:

* [tokio](https://docs.rs/tokio/1.28.0/tokio/): async runtime
    
* [anyhow](https://docs.rs/anyhow/1.0.44/anyhow/): easy error handling
    
* [maplit](https://docs.rs/maplit/1.0.2/maplit/): macro for creating HashMaps
    
* [tracing](https://docs.rs/tracing/0.1.27/tracing/): structured logging
    
* [tracing-subscriber](https://docs.rs/tracing-subscriber/0.2.19/tracing_subscriber/): instrumentation
    

```bash
cargo new webserver
cargo add tokio --features full
cargo add anyhow maplit tracing tracing-subscriber
```

## Anatomy of a simple GET request

In order to actually see what a GET request looks like, we'll set up a simple server listening on port 8080 that will print the incoming requests to the console. This can be done with `netcat`:

```bash
nc -l 8080
```

Now, if we open a new terminal and use `curl` send a simple GET request to our server, we should see the following output:

![](https://raw.githubusercontent.com/geoffreycopin/http_server/gh-pages/blog/img/coloured-get.png align="left")

Let's break down the request parts:

* the method: indicates the action to be performed on the resource. In this case, we are performing a GET request, which means we want to retrieve the resource
    
* the path: uniquely identifies the resource. In this case, we are requesting the root path `/`
    
* the protocol: the protocol version. At this stage, we will always asume HTTP/1.1
    
* the headers: a set of key-value pairs that provide additional information about the request. Our request contains the `Host` header, which indicates the host name of the server, the `User-Agent` header, which describes the client software that is making the request and the `Accept` header, which indicates the media types that are acceptable for the response. We'll go into more details about headers in a later tutorial
    

We'll use the following `struct` to represent requests in our code:

```rust
// req.rs

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Request {
    pub method: Method,
    pub path: String,
    pub headers: HashMap<String, String>,
}

#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Method {
    Get,
}
```

Parsing the request is just a matter of splitting the request string into lines. The first line contains the method, path and protocol separated by spaces. The following lines contain the headers, followed by an empty line.

```rust
// req.rs
use std::{collections::HashMap, hash::Hash};

use tokio::io::{AsyncBufRead, AsyncBufReadExt};

// [...]

impl TryFrom<&str> for Method {
    type Error = anyhow::Error;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        match value {
            "GET" => Ok(Method::Get),
            m => Err(anyhow::anyhow!("unsupported method: {m}")),
        }
    }
}

pub async fn parse_request(mut stream: impl AsyncBufRead + Unpin) -> anyhow::Result<Request> {
    let mut line_buffer = String::new();
    stream.read_line(&mut line_buffer).await?;

    let mut parts = line_buffer.split_whitespace();

    let method: Method = parts
        .next()
        .ok_or(anyhow::anyhow!("missing method"))
        .and_then(TryInto::try_into)?;

    let path: String = parts
        .next()
        .ok_or(anyhow::anyhow!("missing path"))
        .map(Into::into)?;

    let mut headers = HashMap::new();

    loop {
        line_buffer.clear();
        stream.read_line(&mut line_buffer).await?;

        if line_buffer.is_empty() || line_buffer == "\n" || line_buffer == "\r\n" {
            break;
        }

        let mut comps = line_buffer.split(":");
        let key = comps.next().ok_or(anyhow::anyhow!("missing header name"))?;
        let value = comps
            .next()
            .ok_or(anyhow::anyhow!("missing header value"))?
            .trim();

        headers.insert(key.to_string(), value.to_string());
    }

    Ok(Request {
        method,
        path,
        headers,
    })
}
```

## Accepting connections

Now that we know how to parse a request, we can start accepting connections. Each time a new connection is established, we'll spawn a new task to handle it in order to keep the main thread free to accept new connections.

```rust
// main.rs
use tokio::{io::BufStream, net::TcpListener};
use tracing::info;

mod req;

static DEFAULT_PORT: &str = "8080";

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Initialize the default tracing subscriber.
    tracing_subscriber::fmt::init();

    let port: u16 = std::env::args()
        .nth(1)
        .unwrap_or_else(|| DEFAULT_PORT.to_string())
        .parse()?;

    let listener = TcpListener::bind(format!("0.0.0.0:{port}")).await.unwrap();

    info!("listening on: {}", listener.local_addr()?);

    loop {
        let (stream, addr) = listener.accept().await?;
        let mut stream = BufStream::new(stream);

        // do not block the main thread, spawn a new task
        tokio::spawn(async move {
            info!(?addr, "new connection");

            match req::parse_request(&mut stream).await {
                Ok(req) => info!(?req, "incoming request"),
                Err(e) => {
                    info!(?e, "failed to parse request");
                }
            }
        });
    }
}
```

We can now run our server on port `8081`with the following command: `cargo run -- 8081`. Sending a GET request to `localhost:8081` should print the following output:

```plaintext
INFO http_server: listening on: 0.0.0.0:8081
INFO http_server: new connection addr=127.0.0.1:49351
INFO http_server: incoming request req=Request { method: Get, path: "/", headers: {"Host": "localhost", "User-Agent": "curl/7.87.0", "Accept": "*/*"} }
```

## Sending a response

At this stage, we'll answer every request with a static `Not found` page. Our response will have the following format:

![](https://raw.githubusercontent.com/geoffreycopin/http_server/gh-pages/blog/img/coloured-response.png align="left")

Let's explore the different parts of the response:

* the status line: contains the protocol version, the status code and a human-readable status message
    
* the response headers: encoded in the same way as for the request. Our response contains the `Content-Length` header, which specified the length of the response body, and the `Content-Type` header, which indicates that the response body is encoded in HTML. The headers are followed by an empty line.
    
* the response body: contains the actual data that will be displayed in the browser. We used an empty HTML document for brevity
    

We'll use the following `struct` to represent responses in our code:

```rust
// resp.rs
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};

#[derive(Debug, Clone)]
pub struct Response<S: AsyncRead + Unpin> {
    pub status: Status,
    pub headers: HashMap<String, String>,
    pub data: S,
}

#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Status {
    NotFound,
}

impl Display for Status {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Status::NotFound => write!(f, "404 Not Found"),
        }
    }
}
```

The `data` field is generic over the type of the response body to account for future use cases where we might want to send a stream of data.

Creating a response from an HTML string is straight forward:

```rust
// resp.rs
use std::io::Cursor;
use maplit::hashmap;

// [..]

impl Response<Cursor<Vec<u8>>> {
    pub fn from_html(status: Status, data: impl ToString) -> Self {
        let bytes = data.to_string().into_bytes();

        let headers = hashmap! {
            "Content-Type".to_string() => "text/html".to_string(),
            "Content-Length".to_string() => bytes.len().to_string(),
        };

        Self {
            status,
            headers,
            data: Cursor::new(bytes),
        }
    }
}
```

Sending a response is a bit more involved. We'll use the `AsyncWrite` trait to write the response to a generic output stream.

```rust
// resp.rs
use std::{
    collections::HashMap,
    fmt::{Display, Formatter},
    io::Cursor,
};

use maplit::hashmap;
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};

// [...]

impl<S: AsyncRead + Unpin> Response<S> {
    pub fn status_and_headers(&self) -> String {
        let headers = self
            .headers
            .iter()
            .map(|(k, v)| format!("{}: {}", k, v))
            .collect::<Vec<_>>()
            .join("\r\n");

        format!("HTTP/1.1 {}\r\n{headers}\r\n\r\n", self.status)
    }

    pub async fn write<O: AsyncWrite + Unpin>(mut self, stream: &mut O) -> anyhow::Result<()> {
        stream
            .write_all(self.status_and_headers().as_bytes())
            .await?;

        tokio::io::copy(&mut self.data, stream).await?;

        Ok(())
    }
}

impl Display for Status {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Status::NotFound => write!(f, "404 Not Found"),
        }
    }
}
```

## Puting it all together

We'll use the following document as our `404` page:

```html
<!-- static/404.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Page Not Found</title>
    <style>
        body {
            background-color: #f8f8f8;
            font-family: Arial, sans-serif;
            font-size: 16px;
            color: #333;
        }
        .container {
            max-width: 600px;
            margin: 0 auto;
            padding: 40px 20px;
            text-align: center;
            border: 1px solid #ddd;
            border-radius: 5px;
            background-color: #fff;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        h1 {
            font-size: 48px;
            margin-bottom: 20px;
            color: #333;
        }
        p {
            font-size: 24px;
            margin-bottom: 40px;
        }
    </style>
</head>
<body>
<div class="container">
    <h1>404</h1>
    <p>The page you are looking for could not be found.</p>
</div>
</body>
</html>
```

We can now use our `Response` struct to send a `Not found` page to the client when we receive a request:

```rust
// main.rs
// [...]
let resp = resp::Response::from_html(
    resp::Status::NotFound,
    include_str!("../static/404.html"),
);

resp.write(&mut stream).await.unwrap();
// [...]
```

Navigating to `localhost:8081` should now display our `Not found` page.

That's a good start, but we're still far from a fully functional web server. In the next part, we'll add support for serving static files. You can find the code for this part [here](https://github.com/geoffreycopin/http_server).

Looking for a Rust dev? [Let's get in touch!](mailto:copin.geoffrey@gmail.com)
