Let's Build HTTP Parser From Scratch In Rust
Tags: rust, http
Published: 2024-06-04 21:47:08

A week ago, I stumbled upon [this fantastic article](https://www.freecodecamp.org/news/rust-tutorial-build-a-json-parser/) about learning Rust's iterators and pattern matching by building a JSON parser. What really resonated with me was how it explained using Rust's built-in traits. If you're also learning Rust, I highly recommend checking it out!

And now, I want to apply what I learned to other problem.

Well, technically it's not a problem but, you know... anyway I decided to write my own HTTP parser in Rust!

> 2024-06-06 Update:
> I realized that I didn't use some that I learned through the blog post above (say, lexer). So my apologies if I got you disappointed. However I think my version is more performant than that. Enjoy :)

## Program Description

In this article we're going to implement two public structs `HTTPRequest` and `HTTPResponse` .

[You can see the full code here](https://github.com/hirotake111/rust_http_parser)

## HTTP Request

So how does our `HTTPRequest` look like? According to [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages#http_requests) each HTTP request consists of the following elements:

- Request line
- Headers
- And body (this could be empty)

So our `HTTPRequest` struct should look like as follows:

```rust
#[derive(Debug, Clone)]
pub struct HTTPRequest {
    request_line: RequestLine,
    headers: HTTPHeaders,
    body: Option<String>,
}
```

And the corresponding elements should look like as follows:

```rust
#[derive(Debug, Clone)]
pub struct RequestLine {
    method: Method,
    request_target: String,
    http_version: String,
}

#[derive(Debug, Clone)]
struct HTTPHeaders(HashMap<String, String>);

#[derive(Debug, Clone)]
pub enum Method {
    GET,
    POST,
    HEAD,
    OPTIONS,
    DELETE,
    PUT,
    CONNECT,
    TRACE,
}
```

## TryFrom trait

If we implement `TryForm` trait for our `HTTPRequest` struct, we can initialize the struct by performing `HTTPRequest::try_from(input);`.
Or, we could get the same result with `input.try_into()` (I prefer latter).

Also, our input for the HTTP request should be a byte stream as it comes through TCP socket (or other byte stream producers). So it would be great to have `BufReader<T>` as a parameter for a method to initialize the struct.

Overall, our `HTTPRequest` implementation should look like:

```rust
impl<R: Read> TryFrom<BufReader<R>> for HTTPRequest {
    type Error = String;

    fn try_from(reader: BufReader<R>) -> Result<Self, Self::Error> {
        let mut iterator = reader.lines().map_while(Result::ok).peekable();
        let request_line = iterator
            .next()
            .ok_or("failed to get request line")?
            .parse()?;
        let headers = HTTPHeaders::new(&mut iterator)?;
        let body = if iterator.peek().is_some() {
            Some(iterator.collect())
        } else {
            None
        };

        Ok(HTTPRequest {
            request_line,
            headers,
            body,
        })
    }
}
```

It looks clean and tidy :)

And here is an example of other element's implementation under `HTTPRequest`:

```rust
impl FromStr for RequestLine {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut iterator = s.split(' ');
        let method: Method = iterator
            .next()
            .ok_or("failed to get HTTP method")?
            .parse()?;
        let request_target = iterator
            .next()
            .ok_or("failed to get request target")?
            .to_string();
        let http_version = iterator
            .next()
            .ok_or("failed to get HTTP version")?
            .to_string();
        Ok(RequestLine {
            method,
            request_target,
            http_version,
        })
    }
}
```

## HTTP Response

The implementation of `HTTPResponse` looks almost identical to `HTTPRequest` - therefore allow me to omit the detail :)

## Check to see if it works

I prepare a txt file containing HTTP message in it. So let's try and see if it works as expected:

```rust
fn main() {
    let file = std::fs::File::open("examples/request_post.txt").unwrap();
    let reader = BufReader::new(file);
    let request: HTTPRequest = reader.try_into().unwrap();
    dbg!(request);
}
```
And then run it.

```bash
cargo run --example request_get   # GET  request
```

You should see the output as follows:

![output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e7ti1cb5dqe7c3nbuqlu.png)

## Summary

Implementing HTTP parser is definitely easier than JSON. But I learned a lot by making my own from scratch.

Thanks!