Let's Build HTTP Parser From Scratch In Rust

hiro_111

hiro

Posted on June 4, 2024

Let's Build HTTP Parser From Scratch In Rust

A week ago, I stumbled upon this fantastic article 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

HTTP Request

So how does our HTTPRequest look like? According to MDN 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:

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

And the corresponding elements should look like as follows:

#[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,
}
Enter fullscreen mode Exit fullscreen mode

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:

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,
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

It looks clean and tidy :)

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

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,
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

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);
}
Enter fullscreen mode Exit fullscreen mode

And then run it.

cargo run --example request_get   # GET  request
Enter fullscreen mode Exit fullscreen mode

You should see the output as follows:

output

Summary

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

Thanks!

💖 💪 🙅 🚩
hiro_111
hiro

Posted on June 4, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related