Skip to content

Commit 42e2a1b

Browse files
committed
feat: ditched handwritten http parsing for crate httparse
Signed-off-by: Martin <martin@hotmail.com.br>
1 parent 3566cb9 commit 42e2a1b

File tree

3 files changed

+34
-59
lines changed

3 files changed

+34
-59
lines changed

benches/http_parse.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ fn bench_http_parsing(c: &mut Criterion) {
88
let mut group = c.benchmark_group("http_parse");
99

1010
group.bench_function(BenchmarkId::new("My function", "sample http"), |c| {
11-
c.iter(|| parse_http(SAMPLE))
11+
c.iter(|| parse_http(black_box(SAMPLE)))
1212
});
1313
group.bench_function(BenchmarkId::new("HTTP parse", "sample http"), |c| {
1414
c.iter(move || {
15-
let mut headers = [httparse::EMPTY_HEADER; 16];
15+
let mut headers = [httparse::EMPTY_HEADER; 4];
1616
let mut req = Request::new(&mut headers);
17-
ParserConfig::default().parse_request(&mut req, SAMPLE);
17+
ParserConfig::default().parse_request(&mut req, black_box(SAMPLE));
1818
assert_eq!(req.path, Some("/somepath"));
1919
})
2020
});

justfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
flamegraph:
22
export CARGO_PROFILE_RELEASE_DEBUG=true; cargo +nightly flamegraph
3+
4+
bench:
5+
cargo +nightly bench

src/server_impl/server.rs

Lines changed: 28 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use compact_str::CompactString;
44
use either::Either;
55
use eyre::{anyhow, bail, OptionExt};
66
use fnv::FnvHashMap;
7+
use httparse::{ParserConfig, Status};
78
use memchr::{memchr, memmem};
89
use regex_lite::{Match, Regex};
910
use std::str::FromStr;
@@ -121,27 +122,11 @@ fn to_str(str_like: &[u8]) -> &str {
121122
unsafe { std::str::from_utf8_unchecked(str_like) }
122123
}
123124

124-
fn parse_headers(headers: &[u8]) -> FnvHashMap<Header, &str> {
125-
headers
126-
.split(|c| *c == b'\n')
127-
.filter_map(|hdr_line| {
128-
let idx = memchr::memchr(b':', hdr_line)?;
129-
Some((&hdr_line[..idx], &hdr_line[idx + 1..]))
130-
})
131-
.flat_map(|(header, content)| {
132-
let header_str = to_str(header);
133-
134-
let Ok(header) = header_str.parse::<Header>() else {
135-
return None;
136-
};
137-
138-
let content = to_str(content).trim();
139-
Some((header, content))
140-
})
141-
.collect::<FnvHashMap<_, _>>()
142-
}
143-
144125
fn parse_body(body: &[u8]) -> Option<&str> {
126+
if body.is_empty() {
127+
return None;
128+
}
129+
145130
if body.first() == Some(&b'\0') {
146131
None
147132
} else {
@@ -156,33 +141,28 @@ fn parse_body(body: &[u8]) -> Option<&str> {
156141
/// And probably explode if anything else than a well-formed request is parsed.
157142
#[inline]
158143
pub fn parse_http(request: &[u8]) -> AnyResult<Request> {
159-
// https://www.rfc-editor.org/rfc/rfc9110.html#name-protocol-version
160-
let Some(index) = memmem::find(request, b"HTTP/1.1") else {
161-
bail!("err");
162-
};
163-
164-
let method_and_resource = &request[..index];
165-
let rest = &request[index..];
166-
167-
let mut split = method_and_resource.split(|c| c.is_ascii_whitespace());
168-
let method = split
169-
.next()
170-
.map(Method::try_from)
171-
.ok_or_eyre("Malformed request.")?
172-
.map_err(|_| anyhow!("Unknown http method."))?;
173-
let resource = split.next().ok_or_eyre("Could not find resource.")?;
174-
175-
let maybe_body = memmem::find(rest, b"\r\n\r\n").map(|idx| (&rest[..idx], &rest[idx + 4..]));
176-
177-
let (headers, body) = if let Some((headers, body)) = maybe_body {
178-
(parse_headers(headers), parse_body(body))
179-
} else {
180-
(parse_headers(rest), None)
144+
let mut headers = [httparse::EMPTY_HEADER; 4];
145+
let mut req = httparse::Request::new(&mut headers);
146+
let body = ParserConfig::default()
147+
.parse_request(&mut req, request)
148+
.unwrap();
149+
150+
let method = Method::from_str(req.method.unwrap()).unwrap();
151+
let resource = req.path.unwrap();
152+
let headers = req
153+
.headers
154+
.iter()
155+
.map(|c| (Header::from_str(c.name).unwrap(), to_str(c.value)))
156+
.collect::<FnvHashMap<_, _>>();
157+
158+
let body = match body {
159+
Status::Complete(idx) => parse_body(&request[idx..]),
160+
Status::Partial => unimplemented!(),
181161
};
182162

183163
Ok(Request {
184164
method,
185-
resource: to_str(resource),
165+
resource,
186166
headers,
187167
body,
188168
})
@@ -199,29 +179,21 @@ mod tests {
199179
let request = parse_http(sample).unwrap();
200180
assert_eq!(request.method, Method::GET);
201181
assert_eq!(request.resource, "/somepath");
202-
assert_eq!(request.headers.get(&Header::HOST), Some(&"ifconfig.me"));
182+
assert_eq!(
183+
request.headers.get(&Header::CONTENT_TYPE),
184+
Some(&"text/html; charset=ISO-8859-4")
185+
);
203186
assert_eq!(request.body, Some(r#"{"json_key": 10}"#));
204187
}
205188

206189
#[test]
207190
fn success_without_body() {
208-
let sample = b"GET /somepath HTTP/1.1\nHost: ifconfig.me\nUser-Agent: curl/8.5.0\nAccept: */*\nContent-Type: text/html; charset=ISO-8859-4\n";
191+
let sample = b"GET /somepath HTTP/1.1\nHost: ifconfig.me\nUser-Agent: curl/8.5.0\nAccept: */*\nContent-Type: text/html; charset=ISO-8859-4\r\n\r\n";
209192

210193
let request = parse_http(sample).unwrap();
211194
assert_eq!(request.method, Method::GET);
212195
assert_eq!(request.resource, "/somepath");
213196
assert_eq!(request.headers.get(&Header::HOST), Some(&"ifconfig.me"));
214197
assert_eq!(request.body, None);
215198
}
216-
217-
#[test]
218-
fn success_no_route() {
219-
let sample = b"GET /clientes/1/transacao HTTP/1.1\nHost: localhost\nUser-Agent: curl/8.5.0\nAccept: */*\nContent-Type: text/html; charset=ISO-8859-4\r\n\r\n{\"json_key\": 10}";
220-
221-
let request = parse_http(sample).unwrap();
222-
assert_eq!(request.method, Method::GET);
223-
assert_eq!(request.resource, "/somepath");
224-
assert_eq!(request.headers.get(&Header::HOST), Some(&"ifconfig.me"));
225-
assert_eq!(request.body, Some(r#"{"json_key": 10}"#));
226-
}
227199
}

0 commit comments

Comments
 (0)