1use std::fmt;
2
3const SENSITIVE: &[&str] = &[
5 "authorization",
6 "proxy-authorization",
7 "cookie",
8 "set-cookie",
9 "x-api-key",
10 "x-auth-token",
11];
12
13#[derive(Clone, Default, PartialEq, Eq)]
18pub struct Headers(Vec<(String, String)>);
19
20impl Headers {
21 pub fn new() -> Self {
22 Self::default()
23 }
24
25 pub fn insert(&mut self, name: impl Into<String>, value: impl Into<String>) {
26 let name = name.into().to_ascii_lowercase();
27 self.0.retain(|(existing, _)| existing != &name);
28 self.0.push((name, value.into()));
29 }
30
31 pub fn append(&mut self, name: impl Into<String>, value: impl Into<String>) {
32 self.0
33 .push((name.into().to_ascii_lowercase(), value.into()));
34 }
35
36 pub fn get(&self, name: &str) -> Option<&str> {
37 let name = name.to_ascii_lowercase();
38 self.0
39 .iter()
40 .find(|(existing, _)| existing == &name)
41 .map(|(_, value)| value.as_str())
42 }
43
44 pub fn get_u64(&self, name: &str) -> Option<u64> {
48 self.get(name)?.trim().parse().ok()
49 }
50
51 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
52 self.0
53 .iter()
54 .map(|(name, value)| (name.as_str(), value.as_str()))
55 }
56
57 pub fn is_empty(&self) -> bool {
58 self.0.is_empty()
59 }
60
61 pub fn len(&self) -> usize {
62 self.0.len()
63 }
64}
65
66impl<K: Into<String>, V: Into<String>> FromIterator<(K, V)> for Headers {
67 fn from_iter<I: IntoIterator<Item = (K, V)>>(entries: I) -> Self {
68 let mut headers = Self::new();
69 for (name, value) in entries {
70 headers.append(name, value);
71 }
72 headers
73 }
74}
75
76pub(crate) struct RedactedBody(pub(crate) usize);
82
83impl fmt::Debug for RedactedBody {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 write!(f, "<{} bytes, redacted>", self.0)
86 }
87}
88
89impl fmt::Debug for Headers {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 let mut map = f.debug_map();
96 for (name, value) in self.iter() {
97 if SENSITIVE.contains(&name) {
98 map.entry(&name, &"***");
99 } else {
100 map.entry(&name, &value);
101 }
102 }
103 map.finish()
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn lookup_ignores_case() {
113 let mut headers = Headers::new();
114 headers.insert("Content-Type", "application/json");
115
116 assert_eq!(headers.get("content-type"), Some("application/json"));
117 assert_eq!(headers.get("CONTENT-TYPE"), Some("application/json"));
118 }
119
120 #[test]
121 fn insert_replaces_while_append_keeps_both() {
122 let mut headers = Headers::new();
123 headers.insert("x-test", "a");
124 headers.insert("x-test", "b");
125 assert_eq!(headers.len(), 1);
126
127 headers.append("x-test", "c");
128 assert_eq!(headers.len(), 2);
129 }
130
131 #[test]
132 fn debug_output_redacts_credentials() {
133 let mut headers = Headers::new();
134 headers.insert("Authorization", "Bearer ghp_supersecret");
135 headers.insert("Accept", "application/json");
136
137 let rendered = format!("{headers:?}");
138
139 assert!(!rendered.contains("ghp_supersecret"), "got: {rendered}");
140 assert!(rendered.contains("application/json"));
141 }
142
143 #[test]
144 fn a_malformed_numeric_header_is_ignored_rather_than_fatal() {
145 let headers = Headers::from_iter([("x-ratelimit-remaining", "unknown")]);
146 assert_eq!(headers.get_u64("x-ratelimit-remaining"), None);
147 }
148}