coa_website/
request.rs

1use std::collections::HashMap;
2use std::io::Error as IoError;
3
4use tokio::io::AsyncBufRead;
5
6use crate::http::{HttpMethod, HttpVersion};
7
8/// Represents an HTTP request received by a client
9#[derive(Debug)]
10pub struct HttpRequest {
11    /// HTTP Method
12    pub method: HttpMethod,
13    /// Path/Resource requested
14    pub path: String,
15    /// HTTP Version
16    pub version: HttpVersion,
17    /// Headers in lowercase
18    pub headers: HashMap<String, String>,
19}
20
21/// Errors when parsing an HTTP request
22#[derive(Debug)]
23pub enum RequestError {
24    Io(IoError),
25    MalformedRequest,
26    UnsupportedMethod,
27    UnsupportedVersion,
28}
29
30impl From<IoError> for RequestError {
31    fn from(e: IoError) -> Self {
32        RequestError::Io(e)
33    }
34}
35
36impl HttpRequest {
37    /// Parses a raw request line (first line of an HTTP request) into an `HttpRequest`.
38    ///
39    /// Example of a valid request line:
40    /// ```
41    /// use coa_website::request::HttpRequest;
42    /// use coa_website::http::{HttpMethod, HttpVersion};
43    ///
44    /// let line = "GET /index.html HTTP/1.1";
45    /// let (method, path, version) = HttpRequest::parse_line(line).unwrap();
46    /// assert_eq!(method, HttpMethod::GET);
47    /// assert_eq!(path, "/index.html");
48    /// assert_eq!(version, HttpVersion::Http1_1);
49    /// ```
50    ///
51    /// # Errors
52    /// Returns an error if the request line is malformed or unsupported.
53    pub fn parse_line(line: &str) -> Result<(HttpMethod, String, HttpVersion), RequestError> {
54        let parts: Vec<&str> = line.split_whitespace().collect();
55        if parts.len() != 3 {
56            return Err(RequestError::MalformedRequest);
57        }
58
59        let method = parts[0]
60            .parse::<HttpMethod>()
61            .map_err(|_| RequestError::UnsupportedMethod)?;
62        let path = parts[1].to_string();
63        let version = parts[2]
64            .parse::<HttpVersion>()
65            .map_err(|_| RequestError::UnsupportedVersion)?;
66
67        Ok((method, path, version))
68    }
69
70    /// Asynchronously reads and parses a full HTTP request from the provided reader
71    pub async fn from_async_reader(reader: &mut (impl AsyncBufRead + Unpin)) -> Result<Self, RequestError> {
72        use tokio::io::AsyncBufReadExt;
73
74        let mut line = String::new();
75        reader.read_line(&mut line).await?; // Read request line
76        let (method, path, version) = Self::parse_line(&line)?;
77
78        let mut headers = HashMap::new();
79        loop {
80            line.clear();
81            let bytes = reader.read_line(&mut line).await?;
82            if bytes == 0 || line == "\r\n" {
83                break;
84            }
85
86            if let Some((key, value)) = line.split_once(':') {
87                headers.insert(key.trim().to_lowercase(), value.trim().to_string());
88            }
89        }
90
91        Ok(HttpRequest {
92            method,
93            path,
94            version,
95            headers,
96        })
97    }
98
99    /// Gets a header value by lowercase key
100    pub fn header(&self, key: &str) -> Option<&str> {
101        self.headers.get(&key.to_lowercase()).map(String::as_str)
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use tokio::io::BufReader;
109    use tokio::runtime::Runtime;
110
111    #[test]
112    fn test_parse_line_valid() {
113        let (method, path, version) = HttpRequest::parse_line("GET /foo HTTP/1.0").unwrap();
114        assert_eq!(method, HttpMethod::GET);
115        assert_eq!(path, "/foo");
116        assert_eq!(version, HttpVersion::Http1_0);
117    }
118
119    #[test]
120    fn test_parse_line_invalid() {
121        assert!(matches!(HttpRequest::parse_line("BAD LINE"), Err(RequestError::MalformedRequest)));
122    }
123
124    #[test]
125    fn test_parse_line_unknown_method() {
126        assert!(matches!(HttpRequest::parse_line("POST / HTTP/1.1"), Err(RequestError::UnsupportedMethod)));
127    }
128
129    #[test]
130    fn test_parse_line_unknown_version() {
131        assert!(matches!(HttpRequest::parse_line("GET / HTTP/2.0"), Err(RequestError::UnsupportedVersion)));
132    }
133
134    #[test]
135    fn test_header_lookup() {
136        let mut req = HttpRequest { method: HttpMethod::GET, path: String::new(), version: HttpVersion::Http1_1, headers: HashMap::new() };
137        req.headers.insert("content-type".into(), "text/html".into());
138        assert_eq!(req.header("Content-Type"), Some("text/html"));
139    }
140
141    #[test]
142    fn test_from_async_reader() {
143        let rt = Runtime::new().unwrap();
144        rt.block_on(async {
145            let data = b"HEAD /test HTTP/1.1\r\nHost: example.com\r\n\r\n";
146            let mut reader = BufReader::new(&data[..]);
147            let req = HttpRequest::from_async_reader(&mut reader).await.unwrap();
148            assert_eq!(req.method, HttpMethod::HEAD);
149            assert_eq!(req.path, "/test");
150            assert_eq!(req.version, HttpVersion::Http1_1);
151            assert_eq!(req.header("host"), Some("example.com"));
152        });
153    }
154
155    #[test]
156    fn test_from_async_reader_malformed() {
157        let rt = Runtime::new().unwrap();
158        rt.block_on(async {
159            let data = b"INVALID\r\n";
160            let mut reader = BufReader::new(&data[..]);
161            assert!(matches!(HttpRequest::from_async_reader(&mut reader).await, Err(RequestError::MalformedRequest)));
162        });
163    }
164}