rust 55 lines · 9 steps

Parsing an HTTP request head in Rust

A byte-slice parser that turns raw request bytes into a structured head, distinguishing incomplete input from malformed input.

Explained by highlit
1use std::collections::HashMap;
2 
3#[derive(Debug)]
4pub struct RequestHead {
5 pub method: String,
6 pub target: String,
7 pub version: String,
8 pub headers: HashMap<String, String>,
9}
10 
11#[derive(Debug)]
12pub enum ParseError {
13 Incomplete,
14 Malformed(&'static str),
15}
16 
17pub fn parse_head(buf: &[u8]) -> Result<(RequestHead, usize), ParseError> {
18 let end = find_headers_end(buf).ok_or(ParseError::Incomplete)?;
19 let text = std::str::from_utf8(&buf[..end]).map_err(|_| ParseError::Malformed("invalid utf-8"))?;
20 
21 let mut lines = text.split("\r\n");
22 let request_line = lines.next().ok_or(ParseError::Malformed("missing request line"))?;
23 
24 let mut parts = request_line.split(' ');
25 let method = parts.next().ok_or(ParseError::Malformed("missing method"))?;
26 let target = parts.next().ok_or(ParseError::Malformed("missing target"))?;
27 let version = parts.next().ok_or(ParseError::Malformed("missing version"))?;
28 if parts.next().is_some() {
29 return Err(ParseError::Malformed("trailing request line data"));
30 }
31 
32 let mut headers = HashMap::new();
33 for line in lines {
34 if line.is_empty() {
35 continue;
36 }
37 let (name, value) = line.split_once(':').ok_or(ParseError::Malformed("header missing colon"))?;
38 if name.is_empty() || name.contains(' ') {
39 return Err(ParseError::Malformed("invalid header name"));
40 }
41 headers.insert(name.to_ascii_lowercase(), value.trim().to_owned());
42 }
43 
44 let head = RequestHead {
45 method: method.to_owned(),
46 target: target.to_owned(),
47 version: version.to_owned(),
48 headers,
49 };
50 Ok((head, end + 4))
51}
52 
53fn find_headers_end(buf: &[u8]) -> Option<usize> {
54 buf.windows(4).position(|w| w == b"\r\n\r\n")
55}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Separating Incomplete from Malformed lets a caller wait for more bytes versus rejecting a bad request.
  2. 2The ? operator chains fallible steps cleanly by mapping each Option or Result into a typed error.
  3. 3Splitting on delimiters with iterators and split_once keeps line parsing concise without manual indexing.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Parsing an HTTP request head in Rust — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code