Skip to main content

io_http/rfc9110/
request.rs

1//! HTTP request type ([RFC 9110 §9]).
2//!
3//! [RFC 9110 §9]: https://www.rfc-editor.org/rfc/rfc9110#section-9
4
5use core::fmt;
6
7use alloc::{
8    string::{String, ToString},
9    vec::Vec,
10};
11
12use url::Url;
13
14use crate::rfc9110::headers::SENSITIVE_HEADERS;
15
16/// An outgoing HTTP request.
17#[derive(Clone)]
18pub struct HttpRequest {
19    pub method: String,
20    pub url: Url,
21    pub headers: Vec<(String, String)>,
22    pub body: Vec<u8>,
23}
24
25impl HttpRequest {
26    /// Builds a new GET request to the given URL.
27    pub fn get(url: Url) -> Self {
28        Self {
29            method: "GET".into(),
30            url,
31            headers: Vec::new(),
32            body: Vec::new(),
33        }
34    }
35
36    /// Appends a header.
37    pub fn header(mut self, name: impl ToString, value: impl ToString) -> Self {
38        self.headers.push((name.to_string(), value.to_string()));
39        self
40    }
41
42    /// Sets the request body.
43    pub fn body(mut self, body: Vec<u8>) -> Self {
44        self.body = body;
45        self
46    }
47}
48
49impl fmt::Debug for HttpRequest {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        let headers: Vec<(&str, &str)> = self
52            .headers
53            .iter()
54            .map(|(k, v)| {
55                let sensitive = SENSITIVE_HEADERS.iter().any(|s| k.eq_ignore_ascii_case(s));
56                let v = if sensitive { "[REDACTED]" } else { v.as_str() };
57                (k.as_str(), v)
58            })
59            .collect();
60
61        f.debug_struct("HttpRequest")
62            .field("method", &self.method)
63            .field("url", &self.url.as_str())
64            .field("headers", &headers)
65            .field("body", &format_args!("[{} bytes]", self.body.len()))
66            .finish()
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use alloc::format;
73
74    use url::Url;
75
76    use crate::rfc9110::request::*;
77
78    #[test]
79    fn get_method_and_empty_body() {
80        let url = Url::parse("http://example.com/path").unwrap();
81        let req = HttpRequest::get(url);
82        assert_eq!(req.method, "GET");
83        assert!(req.body.is_empty());
84        assert!(req.headers.is_empty());
85    }
86
87    #[test]
88    fn header_appended() {
89        let url = Url::parse("http://example.com/").unwrap();
90        let req = HttpRequest::get(url)
91            .header("Host", "example.com")
92            .header("Accept", "text/html");
93        assert_eq!(req.headers.len(), 2);
94        assert_eq!(req.headers[0], ("Host".into(), "example.com".into()));
95        assert_eq!(req.headers[1], ("Accept".into(), "text/html".into()));
96    }
97
98    #[test]
99    fn body_replaces() {
100        let url = Url::parse("http://example.com/").unwrap();
101        let req = HttpRequest::get(url).body(b"hello".to_vec());
102        assert_eq!(req.body, b"hello");
103    }
104
105    #[test]
106    fn debug_redacts_sensitive_headers() {
107        let url = Url::parse("http://example.com/").unwrap();
108        let req = HttpRequest::get(url)
109            .header("Host", "example.com")
110            .header("Authorization", "Bearer secret-token")
111            .header("Cookie", "session=abc123");
112        let debug = format!("{req:?}");
113        assert!(debug.contains("[REDACTED]"), "expected redaction marker");
114        assert!(
115            !debug.contains("secret-token"),
116            "token must not appear in debug"
117        );
118        assert!(
119            !debug.contains("abc123"),
120            "cookie value must not appear in debug"
121        );
122        assert!(
123            debug.contains("example.com"),
124            "non-sensitive header value must appear"
125        );
126    }
127}