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::SENSITIVE_HEADERS;
15
16#[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 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 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 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}