micro_http/protocol/
request.rs

1//! HTTP request header handling implementation.
2//!
3//! This module provides the core abstractions for handling HTTP request headers.
4//! It wraps the standard `http::Request` type to provide additional functionality
5//! specific to our HTTP server implementation.
6
7use http::request::Parts;
8use http::{HeaderMap, Method, Request, Uri, Version};
9
10/// Represents an HTTP request header.
11///
12/// This struct wraps a `http::Request<()>` to provide:
13/// - Access to standard HTTP header fields
14/// - Conversion from different request formats
15/// - Body attachment capabilities
16/// - Request metadata inspection
17#[derive(Debug)]
18pub struct RequestHeader {
19    inner: Request<()>,
20}
21
22impl AsRef<Request<()>> for RequestHeader {
23    fn as_ref(&self) -> &Request<()> {
24        &self.inner
25    }
26}
27
28impl AsMut<Request<()>> for RequestHeader {
29    fn as_mut(&mut self) -> &mut Request<()> {
30        &mut self.inner
31    }
32}
33
34impl RequestHeader {
35    /// Consumes the header and returns the inner `Request<()>`.
36    pub fn into_inner(self) -> Request<()> {
37        self.inner
38    }
39
40    /// Attaches a body to this header, converting it into a full `Request<T>`.
41    ///
42    /// This is typically used after header parsing to attach the parsed body.
43    pub fn body<T>(self, body: T) -> Request<T> {
44        self.inner.map(|_| body)
45    }
46
47    /// Returns a reference to the request's HTTP method.
48    pub fn method(&self) -> &Method {
49        self.inner.method()
50    }
51
52    /// Returns a reference to the request's URI.
53    pub fn uri(&self) -> &Uri {
54        self.inner.uri()
55    }
56
57    /// Returns the request's HTTP version.
58    pub fn version(&self) -> Version {
59        self.inner.version()
60    }
61
62    /// Returns a reference to the request's headers.
63    pub fn headers(&self) -> &HeaderMap {
64        self.inner.headers()
65    }
66
67    /// Determines if this request requires a body based on its HTTP method.
68    ///
69    /// Returns false for methods that typically don't have bodies:
70    /// - GET
71    /// - HEAD
72    /// - DELETE
73    /// - OPTIONS
74    /// - CONNECT
75    pub fn need_body(&self) -> bool {
76        !matches!(
77            self.method(),
78            &Method::GET | &Method::HEAD | &Method::DELETE | &Method::OPTIONS | &Method::CONNECT | &Method::TRACE | &Method::PATCH
79        )
80    }
81}
82
83/// Converts request parts into a RequestHeader.
84impl From<Parts> for RequestHeader {
85    #[inline]
86    fn from(parts: Parts) -> Self {
87        Self { inner: Request::from_parts(parts, ()) }
88    }
89}
90
91/// Converts a bodyless request into a RequestHeader.
92impl From<Request<()>> for RequestHeader {
93    #[inline]
94    fn from(inner: Request<()>) -> Self {
95        Self { inner }
96    }
97}
98
99/// Converts a parsed HTTP request into a RequestHeader.
100///
101/// This implementation handles the conversion from the low-level parsed request
102/// format into our RequestHeader type, setting up:
103/// - HTTP method
104/// - URI/path
105/// - HTTP version
106/// - Headers
107impl<'headers, 'buf> From<httparse::Request<'headers, 'buf>> for RequestHeader {
108    fn from(req: httparse::Request<'headers, 'buf>) -> Self {
109        let mut builder =
110            Request::builder().method(req.method.unwrap()).uri(req.path.unwrap()).version(U8Wrapper(req.version.unwrap()).into());
111
112        builder.headers_mut().unwrap().reserve(req.headers.len());
113        for header in req.headers.iter() {
114            builder = builder.header(header.name, header.value)
115        }
116
117        RequestHeader { inner: builder.body(()).unwrap() }
118    }
119}
120
121/// Helper struct for HTTP version conversion.
122struct U8Wrapper(u8);
123
124impl From<U8Wrapper> for Version {
125    fn from(value: U8Wrapper) -> Self {
126        match value.0 {
127            1 => Version::HTTP_11,
128            0 => Version::HTTP_10,
129            // http2 and http3 currently not support
130            _ => Version::HTTP_09,
131        }
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use std::mem::MaybeUninit;
138
139    use http::HeaderValue;
140    use indoc::indoc;
141
142    use super::*;
143
144    #[test]
145    fn from_curl() {
146        let str = indoc! {r##"
147        GET /index.html HTTP/1.1
148        Host: 127.0.0.1:8080
149        User-Agent: curl/7.79.1
150        Accept: */*
151
152        "##};
153
154        let mut parsed_req = httparse::Request::new(&mut []);
155        let mut headers: [MaybeUninit<httparse::Header>; 4] = unsafe { MaybeUninit::uninit().assume_init() };
156
157        parsed_req.parse_with_uninit_headers(str.as_bytes(), &mut headers).unwrap();
158
159        let header: RequestHeader = parsed_req.into();
160
161        assert_eq!(header.method(), &Method::GET);
162        assert_eq!(header.version(), Version::HTTP_11);
163        assert_eq!(header.uri().host(), None);
164        assert_eq!(header.uri().path(), "/index.html");
165        assert_eq!(header.uri().scheme(), None);
166        assert_eq!(header.uri().query(), None);
167
168        assert_eq!(header.headers().len(), 3);
169
170        assert_eq!(header.headers().get(http::header::ACCEPT), Some(&HeaderValue::from_str("*/*").unwrap()));
171
172        assert_eq!(header.headers().get(http::header::HOST), Some(&HeaderValue::from_str("127.0.0.1:8080").unwrap()));
173
174        assert_eq!(header.headers().get(http::header::USER_AGENT), Some(&HeaderValue::from_str("curl/7.79.1").unwrap()));
175    }
176
177    #[test]
178    fn from_edge() {
179        let str = indoc! {r##"
180        GET /index/?a=1&b=2&a=3 HTTP/1.1
181        Host: 127.0.0.1:8080
182        Connection: keep-alive
183        Cache-Control: max-age=0
184        sec-ch-ua: "#Not_A Brand";v="99", "Microsoft Edge";v="109", "Chromium";v="109"
185        sec-ch-ua-mobile: ?0
186        sec-ch-ua-platform: "macOS"
187        Upgrade-Insecure-Requests: 1
188        User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.52
189        Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
190        Sec-Fetch-Site: none
191        Sec-Fetch-Mode: navigate
192        Sec-Fetch-User: ?1
193        Sec-Fetch-Dest: document
194        Accept-Encoding: gzip, deflate, br
195        Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
196
197        "##};
198
199        let mut parsed_req = httparse::Request::new(&mut []);
200        let mut headers: [MaybeUninit<httparse::Header>; 64] = unsafe { MaybeUninit::uninit().assume_init() };
201
202        parsed_req.parse_with_uninit_headers(str.as_bytes(), &mut headers).unwrap();
203
204        let header: RequestHeader = parsed_req.into();
205
206        assert_eq!(header.method(), &Method::GET);
207        assert_eq!(header.version(), Version::HTTP_11);
208        assert_eq!(header.uri().host(), None);
209        assert_eq!(header.uri().path(), "/index/");
210        assert_eq!(header.uri().scheme(), None);
211        assert_eq!(header.uri().query(), Some("a=1&b=2&a=3"));
212
213        assert_eq!(header.headers().len(), 15);
214
215        // TODO maybe we can using macro to reduce code
216        assert_eq!(header.headers().get(http::header::CONNECTION), Some(&HeaderValue::from_str("keep-alive").unwrap()));
217
218        assert_eq!(header.headers().get(http::header::CACHE_CONTROL), Some(&HeaderValue::from_str("max-age=0").unwrap()));
219
220        assert_eq!(
221            header.headers().get("sec-ch-ua"),
222            Some(&HeaderValue::from_str(r##""#Not_A Brand";v="99", "Microsoft Edge";v="109", "Chromium";v="109""##).unwrap())
223        );
224
225        assert_eq!(header.headers().get("sec-ch-ua-mobile"), Some(&HeaderValue::from_str("?0").unwrap()));
226
227        assert_eq!(header.headers().get("sec-ch-ua-platform"), Some(&HeaderValue::from_str("\"macOS\"").unwrap()));
228
229        assert_eq!(header.headers().get(http::header::UPGRADE_INSECURE_REQUESTS), Some(&HeaderValue::from_str("1").unwrap()));
230
231        assert_eq!(header.headers().get(http::header::USER_AGENT),
232                   Some(&HeaderValue::from_str("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.52").unwrap()));
233
234        assert_eq!(header.headers().get("Sec-Fetch-Site"), Some(&HeaderValue::from_str("none").unwrap()));
235
236        assert_eq!(header.headers().get("Sec-Fetch-Mode"), Some(&HeaderValue::from_str("navigate").unwrap()));
237
238        assert_eq!(header.headers().get("Sec-Fetch-User"), Some(&HeaderValue::from_str("?1").unwrap()));
239
240        assert_eq!(header.headers().get("Sec-Fetch-Dest"), Some(&HeaderValue::from_str("document").unwrap()));
241
242        assert_eq!(header.headers().get(http::header::ACCEPT_ENCODING), Some(&HeaderValue::from_str("gzip, deflate, br").unwrap()));
243
244        assert_eq!(
245            header.headers().get(http::header::ACCEPT_LANGUAGE),
246            Some(&HeaderValue::from_str("zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7").unwrap())
247        );
248    }
249}