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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Separating Incomplete from Malformed lets a caller wait for more bytes versus rejecting a bad request.
- 2The ? operator chains fallible steps cleanly by mapping each Option or Result into a typed error.
- 3Splitting on delimiters with iterators and split_once keeps line parsing concise without manual indexing.
Related explainers
python
import datetime from dataclasses import dataclass
Parsing fixed-width records in Python
parsing
generators
dataclasses
Intermediate
8 steps
php
<?php final class MarkdownParser {
Building a small Markdown-to-HTML parser in PHP
state-machine
parsing
closures
Intermediate
10 steps
rust
#[derive(Debug, Default)] pub struct RequestBuilder { url: String, method: String,
The builder pattern in Rust
builder-pattern
method-chaining
ergonomic-api
Intermediate
8 steps
go
package humanize import ( "fmt"
Parsing human-readable byte sizes in Go
parsing
regex
lookup-table
Intermediate
8 steps
rust
use axum::{extract::{Path, State}, http::StatusCode, Json}; use dashmap::DashMap; use serde::Serialize; use std::sync::Arc;
Request coalescing in an Axum handler
caching
concurrency
request-coalescing
Advanced
8 steps
ruby
class TemplateInterpolator PLACEHOLDER = /\{\{\s*([\w.]+)\s*\}\}/ def initialize(strict: false)
Interpolating templates with dotted keys in Ruby
regex
string-interpolation
hash-traversal
Intermediate
6 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/parsing-an-http-request-head-in-rust-explained-rust-92d1/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.