io_http/rfc9110/
request.rs1use 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#[derive(Clone)]
18pub struct HttpRequest {
19 pub method: String,
21 pub url: Url,
24 pub headers: Vec<(String, String)>,
26 pub body: Vec<u8>,
29}
30
31impl HttpRequest {
32 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 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 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}