Skip to main content

nanofish_client/
request.rs

1use crate::{error::Error, header::HttpHeader, method::HttpMethod};
2use heapless::Vec;
3
4/// Maximum number of headers allowed in a request
5pub const MAX_HEADERS: usize = 16;
6
7/// HTTP request parsed from client
8#[derive(Debug)]
9pub struct HttpRequest<'a> {
10    /// HTTP method
11    pub method: HttpMethod,
12    /// Request path
13    pub path: &'a str,
14    /// HTTP version (e.g., "HTTP/1.1")
15    pub version: &'a str,
16    /// Request headers
17    pub headers: Vec<HttpHeader<'a>, MAX_HEADERS>,
18    /// Request body (if present)
19    pub body: &'a [u8],
20}
21
22/// Find the position of the double CRLF sequence that separates headers from body
23fn find_double_crlf(data: &[u8]) -> Option<usize> {
24    const DOUBLE_CRLF: &[u8] = b"\r\n\r\n";
25    (0..data.len().saturating_sub(3)).find(|&i| &data[i..i + 4] == DOUBLE_CRLF)
26}
27
28impl<'a> HttpRequest<'a> {
29    /// Parse an HTTP request from headers string and body bytes
30    ///
31    /// # Errors
32    ///
33    /// Returns an error if:
34    /// - The request line is missing or malformed
35    /// - The HTTP method is invalid or unsupported  
36    /// - Required parts (method, path, version) are missing
37    /// - Too many headers are provided (exceeds `MAX_HEADERS`)
38    pub fn parse_from(headers_str: &'a str, body: &'a [u8]) -> Result<Self, Error> {
39        let mut lines = headers_str.lines();
40
41        // Parse request line
42        let request_line = lines
43            .next()
44            .ok_or(Error::InvalidResponse("Missing request line"))?;
45        let mut parts = request_line.split_whitespace();
46
47        let method_str = parts
48            .next()
49            .ok_or(Error::InvalidResponse("Missing method"))?;
50        let path = parts.next().ok_or(Error::InvalidResponse("Missing path"))?;
51        let version = parts
52            .next()
53            .ok_or(Error::InvalidResponse("Missing version"))?;
54
55        let method = HttpMethod::try_from(method_str)
56            .map_err(|_| Error::InvalidResponse("Unknown HTTP method"))?;
57
58        // Parse headers
59        let mut headers = Vec::new();
60        for line in lines {
61            if line.is_empty() {
62                break;
63            }
64
65            if let Some(colon_pos) = line.find(':') {
66                let name = line[..colon_pos].trim();
67                let value = line[colon_pos + 1..].trim();
68
69                let header = HttpHeader::new(name, value);
70                headers
71                    .push(header)
72                    .map_err(|_| Error::InvalidResponse("Too many headers"))?;
73            }
74        }
75
76        Ok(HttpRequest {
77            method,
78            path,
79            version,
80            headers,
81            body,
82        })
83    }
84}
85
86impl<'a> TryFrom<&'a [u8]> for HttpRequest<'a> {
87    type Error = Error;
88
89    fn try_from(buffer: &'a [u8]) -> Result<Self, Self::Error> {
90        // Find the end of headers (double CRLF)
91        let end_of_headers =
92            find_double_crlf(buffer).ok_or(Error::InvalidResponse("Incomplete request headers"))?;
93
94        // Parse the headers string
95        let headers_str = core::str::from_utf8(&buffer[..end_of_headers])
96            .map_err(|_| Error::InvalidResponse("Invalid UTF-8 in request"))?;
97
98        // Body starts after the double CRLF
99        let body = &buffer[end_of_headers + 4..];
100
101        Self::parse_from(headers_str, body)
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::HttpMethod;
109
110    #[test]
111    fn test_parse_request_get() {
112        let request_str =
113            "GET /index.html HTTP/1.1\r\nHost: example.com\r\nUser-Agent: test\r\n\r\n";
114        let body = b"";
115
116        let request = HttpRequest::parse_from(request_str, body).unwrap();
117
118        assert_eq!(request.method, HttpMethod::GET);
119        assert_eq!(request.path, "/index.html");
120        assert_eq!(request.version, "HTTP/1.1");
121        assert_eq!(request.headers.len(), 2);
122        assert_eq!(request.body, b"");
123    }
124
125    #[test]
126    fn test_parse_request_post_with_body() {
127        let request_str = "POST /api/data HTTP/1.1\r\nContent-Type: application/json\r\nContent-Length: 13\r\n\r\n";
128        let body = b"{\"key\":\"value\"}";
129
130        let request = HttpRequest::parse_from(request_str, body).unwrap();
131
132        assert_eq!(request.method, HttpMethod::POST);
133        assert_eq!(request.path, "/api/data");
134        assert_eq!(request.version, "HTTP/1.1");
135        assert_eq!(request.headers.len(), 2);
136        assert_eq!(request.body, b"{\"key\":\"value\"}");
137
138        // Check specific headers
139        let content_type_header = request
140            .headers
141            .iter()
142            .find(|h| h.name == "Content-Type")
143            .unwrap();
144        assert_eq!(content_type_header.value, "application/json");
145    }
146
147    #[test]
148    fn test_parse_request_invalid_method() {
149        let request_str = "INVALID /path HTTP/1.1\r\n\r\n";
150        let body = b"";
151
152        let result = HttpRequest::parse_from(request_str, body);
153        assert!(result.is_err());
154    }
155
156    #[test]
157    fn test_parse_request_missing_parts() {
158        // Missing path
159        let request_str = "GET HTTP/1.1\r\n\r\n";
160        let body = b"";
161        let result = HttpRequest::parse_from(request_str, body);
162        assert!(result.is_err());
163
164        // Missing version
165        let request_str = "GET /path\r\n\r\n";
166        let result = HttpRequest::parse_from(request_str, body);
167        assert!(result.is_err());
168
169        // Empty request
170        let request_str = "";
171        let result = HttpRequest::parse_from(request_str, body);
172        assert!(result.is_err());
173    }
174
175    #[test]
176    fn test_parse_request_all_http_methods() {
177        let methods = [
178            ("GET", HttpMethod::GET),
179            ("POST", HttpMethod::POST),
180            ("PUT", HttpMethod::PUT),
181            ("DELETE", HttpMethod::DELETE),
182            ("PATCH", HttpMethod::PATCH),
183            ("HEAD", HttpMethod::HEAD),
184            ("OPTIONS", HttpMethod::OPTIONS),
185            ("TRACE", HttpMethod::TRACE),
186            ("CONNECT", HttpMethod::CONNECT),
187        ];
188
189        for (method_str, expected_method) in &methods {
190            let request_str = format!("{method_str} /path HTTP/1.1\r\n\r\n");
191            let request = HttpRequest::parse_from(&request_str, b"").unwrap();
192            assert_eq!(request.method, *expected_method);
193        }
194    }
195
196    #[test]
197    fn test_find_double_crlf() {
198        // Normal case
199        let data = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\nBody";
200        assert_eq!(find_double_crlf(data), Some(33));
201
202        // At the beginning
203        let data = b"\r\n\r\nBody";
204        assert_eq!(find_double_crlf(data), Some(0));
205
206        // At the end
207        let data = b"Headers\r\n\r\n";
208        assert_eq!(find_double_crlf(data), Some(7));
209
210        // Not found
211        let data = b"GET / HTTP/1.1\r\nHost: example.com\r\n";
212        assert_eq!(find_double_crlf(data), None);
213
214        // Too short
215        let data = b"\r\n\r";
216        assert_eq!(find_double_crlf(data), None);
217
218        // Empty
219        let data = b"";
220        assert_eq!(find_double_crlf(data), None);
221    }
222
223    #[test]
224    fn test_try_from_complete_request() {
225        let buffer = b"GET /index.html HTTP/1.1\r\nHost: example.com\r\nUser-Agent: test\r\n\r\n";
226
227        let request = HttpRequest::try_from(buffer.as_slice()).unwrap();
228
229        assert_eq!(request.method, HttpMethod::GET);
230        assert_eq!(request.path, "/index.html");
231        assert_eq!(request.version, "HTTP/1.1");
232        assert_eq!(request.headers.len(), 2);
233        assert_eq!(request.body, b"");
234    }
235
236    #[test]
237    fn test_try_from_request_with_body() {
238        let buffer =
239            b"POST /api/data HTTP/1.1\r\nContent-Type: application/json\r\n\r\n{\"key\":\"value\"}";
240
241        let request = HttpRequest::try_from(buffer.as_slice()).unwrap();
242
243        assert_eq!(request.method, HttpMethod::POST);
244        assert_eq!(request.path, "/api/data");
245        assert_eq!(request.version, "HTTP/1.1");
246        assert_eq!(request.headers.len(), 1);
247        assert_eq!(request.body, b"{\"key\":\"value\"}");
248    }
249
250    #[test]
251    fn test_try_from_incomplete_headers() {
252        let buffer = b"GET /index.html HTTP/1.1\r\nHost: example.com\r\n";
253
254        let result = HttpRequest::try_from(buffer.as_slice());
255        assert!(result.is_err());
256    }
257
258    #[test]
259    fn test_try_from_invalid_utf8() {
260        // Create buffer with invalid UTF-8 in headers
261        let mut buffer: Vec<u8, 128> = Vec::new();
262        let _ = buffer.extend_from_slice(b"GET /index.html HTTP/1.1\r\nHost: ");
263        let _ = buffer.push(0xFF); // Invalid UTF-8
264        let _ = buffer.extend_from_slice(b"\r\n\r\n");
265
266        let result = HttpRequest::try_from(buffer.as_slice());
267        assert!(result.is_err());
268    }
269}