hiro
Posted on June 4, 2024
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>,
}
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,
}
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,
})
}
}
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,
})
}
}
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);
}
And then run it.
cargo run --example request_get # GET request
You should see the output as follows:
Summary
Implementing HTTP parser is definitely easier than JSON. But I learned a lot by making my own from scratch.
Thanks!
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
September 23, 2024