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!
Top comments (0)