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