request
When we are creating a web server, the first thing that we have to do is handle the incoming requests, this involves reading and parsing those requests. General structure of an HTTP request looks something like this:
<method> <url> <version>
header_1_key: value_1
header_2_key: value_2
<body>
The first line of the request is known as the request line. It contains 3 things,
- method: the HTTP method with which the request was made, e.g GET,POST,DELETE etc
- url: the URL to which the request was made, e.g http://localhost:3000/some/path
- version: the HTTP version, this is usually equal to HTTP/1.1 when dealing with simple web applications
After the request lines, we have headers, which are in simple key:value form and contain data about the request such as the Content-Type, Content-Length etc
After the headers we have an empty line which is followed by the body of the request. The body is optional and only required when dealing with methods like POST,PUT etc
I wrote my own request parser without using any pre-existing libraries and it works like this:
- firstly I create a buffer reader which goes through the entire request line by line
- one important thing to keep in mind is the
Content-Lengthheader, it is extremely important to have before you read the body itself, and that is why while the reader is reading line-by-line I explicitly look for the line which starts withContent-Length:if I find any line that matches this condition, then we just read the value of this header, and store it into a variable calledcontent_length - during the line-by-line reading I also check if the reader runs into any empty lines, if yes then that means it’s time to stop the reader as we have either finished reading the request or we have hit the separationg between headers and body, but we also have to store the empty line too, so that while parsing we can keep track of the separation between the headers and the body
- once we have reached the empty line, if value of
content_lengthis greater than zero then means the request has a body as well, so we use the buffer reader to read exactly that amount of characters from the request and then store it into arequest_bodyvariable
The rust code for doing the above is:
let mut buf_reader = std::io::BufReader::new(value);
let mut request_lines_vector: Vec<String> = Vec::new();
let mut content_length = 0;
for line in buf_reader.by_ref().lines() {
let line = line?;
if let Some(content_length_str) = line.strip_prefix("Content-Length: ") {
content_length = content_length_str.parse()?;
}
if line.is_empty() {
request_lines_vector.push(String::from(""));
break;
}
request_lines_vector.push(line);
}
let mut body_buffer: Vec<u8> = Vec::new();
if content_length > 0 {
body_buffer.resize(content_length, 0);
buf_reader
.take(content_length as u64)
.read_exact(&mut body_buffer)?;
request_lines_vector.push(String::from_utf8_lossy(&body_buffer).to_string());
}
// ----- handle empty request ------
if request_lines_vector.len() == 0 {
return Err(error::Error::EmptyRequestError());
}After reading the entire request now we have to actually parse the request into a data-structure that our code can easily. We follow the same method as reading and parse the request-line, headers, and body separately.
- I get the request-line and use the space between the words to split it into 3 parts, if the split variable does not have exactly 3 parts that means it was a faulty request so total parts should be 3
- first part is the method, second is the path, third is the http version, they are basically string values and you can use them as string values if you want, I just created an enum
HttpMethodthat takes the string and converts it into one of its values - after that we have to parse the headers, we run a loop going through all the remaining lines,
- we are still going through each header line by line and we split each header(just once) by
:so that have the key-value pair, we just create a hashmap of headers and insert these key-value pairs into that hashmap - if we encounter an empty line, that means we have either hit the header-body sepration or it’s the end of request
- the body itself is stored as just a single string and we can just directly put that string into the request data structure
Rust code for diong the above is:
#[derive(Debug)]
pub struct Request {
pub method: http::HttpMethod,
pub path: String,
pub version: String,
pub headers: std::collections::HashMap<String, String>,
pub body: Option<String>,
}
let mut parsed_request = Request::default();
let mut request_lines_iter = request_lines_vector.iter();
// parse request line
match request_lines_iter.next() {
Some(line) => {
let parts = line.split_whitespace().collect::<Vec<&str>>();
if parts.len() != 3 {
return Err(error::Error::InvalidRequestLine());
}
parsed_request.method = http::HttpMethod::from(parts[0]);
parsed_request.path = String::from(parts[1]);
parsed_request.version = String::from(parts[2]);
}
None => {
return Err(error::Error::InvalidRequestLine());
}
}
// parse headers
while let Some(line) = request_lines_iter.next() {
if line.trim().is_empty() {
break;
}
match line.split_once(":") {
Some((key, value)) => {
parsed_request
.headers
.insert(key.trim().to_string(), value.trim().to_string());
}
None => {
return Err(error::Error::InvalidRequestHeader(line.to_string()));
}
}
}
// parse body
parsed_request.body = request_lines_iter.next().cloned();response
The general structure of a response looks like this:
<version> <status_code> <reason-phrase>
header_1_key: value_1
header_2_key: value_2
<body>