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