Skip to main content

ferrin_spec/shared/
headers.rs

1//! Header map wrapper with merge semantics.
2
3use std::fmt;
4
5use http::HeaderMap;
6use http::HeaderName;
7use http::HeaderValue;
8use http::header::USER_AGENT;
9use serde::Deserialize;
10use serde::Serialize;
11
12/// Header names whose values are masked in `Debug` and serialized output.
13const SENSITIVE_HEADERS: &[&str] = &[
14    "authorization",
15    "proxy-authorization",
16    "x-api-key",
17    "api-key",
18    "x-goog-api-key",
19    "cookie",
20    "set-cookie",
21];
22
23/// Placeholder used in place of a sensitive header value.
24const MASKED: &str = "***";
25
26/// Request or response headers.
27///
28/// A thin wrapper around [`http::HeaderMap`] that adds provider-oriented
29/// helpers: overriding merge, user-agent suffixing and masking of sensitive
30/// values in diagnostic output. `Debug` and `Serialize` never reveal
31/// authorization, API-key or cookie values.
32#[derive(Clone, Default, PartialEq, Eq)]
33pub struct Headers(HeaderMap);
34
35impl Headers {
36    /// Creates an empty header map.
37    #[must_use]
38    pub fn new() -> Self {
39        Self(HeaderMap::new())
40    }
41
42    /// Wraps an existing [`HeaderMap`].
43    #[must_use]
44    pub fn from_map(map: HeaderMap) -> Self {
45        Self(map)
46    }
47
48    /// Returns the wrapped [`HeaderMap`].
49    #[must_use]
50    pub fn as_map(&self) -> &HeaderMap {
51        &self.0
52    }
53
54    /// Returns the wrapped [`HeaderMap`] mutably.
55    pub fn as_map_mut(&mut self) -> &mut HeaderMap {
56        &mut self.0
57    }
58
59    /// Consumes the wrapper and returns the [`HeaderMap`].
60    #[must_use]
61    pub fn into_map(self) -> HeaderMap {
62        self.0
63    }
64
65    /// Returns `true` when no header is present.
66    #[must_use]
67    pub fn is_empty(&self) -> bool {
68        self.0.is_empty()
69    }
70
71    /// Returns the number of header entries (repeated names count once each).
72    #[must_use]
73    pub fn len(&self) -> usize {
74        self.0.len()
75    }
76
77    /// Returns the first value of `name` as a string, if present and ASCII.
78    #[must_use]
79    pub fn get_str(&self, name: &str) -> Option<&str> {
80        self.0.get(name).and_then(|value| value.to_str().ok())
81    }
82
83    /// Returns `true` when `name` is present.
84    #[must_use]
85    pub fn contains(&self, name: &str) -> bool {
86        self.0.contains_key(name)
87    }
88
89    /// Inserts a header, replacing existing values with the same name.
90    ///
91    /// Returns an error when the name or value is not a valid header.
92    ///
93    /// # Errors
94    ///
95    /// Returns [`InvalidHeader`] when `name` is not a valid header name or
96    /// `value` contains characters not allowed in header values.
97    pub fn insert(&mut self, name: &str, value: &str) -> Result<(), InvalidHeader> {
98        let name = HeaderName::from_bytes(name.as_bytes()).map_err(|_| InvalidHeader {
99            name: name.to_owned(),
100        })?;
101        let value = HeaderValue::from_str(value).map_err(|_| InvalidHeader {
102            name: name.to_string(),
103        })?;
104        self.0.insert(name, value);
105        Ok(())
106    }
107
108    /// Builder-style [`Headers::insert`]; invalid pairs are skipped silently.
109    ///
110    /// Intended for static configuration where the caller controls the values;
111    /// use [`Headers::insert`] for untrusted input.
112    #[must_use]
113    pub fn with(mut self, name: &str, value: &str) -> Self {
114        let _ = self.insert(name, value);
115        self
116    }
117
118    /// Removes all values for `name`.
119    pub fn remove(&mut self, name: &str) {
120        self.0.remove(name);
121    }
122
123    /// Merges `other` into `self`; values in `other` override existing ones.
124    ///
125    /// Repeated header names in `other` replace all existing values of that
126    /// name, mirroring "later headers win" semantics for configuration
127    /// layering (provider defaults, per-model settings, per-call headers).
128    pub fn merge(&mut self, other: &Headers) {
129        let mut current: Option<HeaderName> = None;
130        for (name, value) in &other.0 {
131            if current.as_ref() != Some(name) {
132                self.0.remove(name);
133                current = Some(name.clone());
134            }
135            self.0.append(name.clone(), value.clone());
136        }
137    }
138
139    /// Returns a copy with `other` merged on top; see [`Headers::merge`].
140    #[must_use]
141    pub fn merged(mut self, other: &Headers) -> Self {
142        self.merge(other);
143        self
144    }
145
146    /// Merges optional string pairs; pairs whose value is `None` are skipped.
147    ///
148    /// Invalid names or values are skipped as well; this mirrors the lenient
149    /// behaviour needed when layering user-supplied header maps.
150    pub fn merge_pairs<'a, I>(&mut self, pairs: I)
151    where
152        I: IntoIterator<Item = (&'a str, Option<&'a str>)>,
153    {
154        for (name, value) in pairs {
155            if let Some(value) = value {
156                let _ = self.insert(name, value);
157            }
158        }
159    }
160
161    /// Appends `suffixes` to the `user-agent` header, creating it if absent.
162    ///
163    /// Existing user-agent text is kept and each suffix is separated by a
164    /// single space. Empty suffixes are ignored.
165    pub fn with_user_agent_suffix<'a, I>(mut self, suffixes: I) -> Self
166    where
167        I: IntoIterator<Item = &'a str>,
168    {
169        let mut parts: Vec<String> = Vec::new();
170        if let Some(existing) = self.get_str(USER_AGENT.as_str()) {
171            let existing = existing.trim();
172            if !existing.is_empty() {
173                parts.push(existing.to_owned());
174            }
175        }
176        parts.extend(
177            suffixes
178                .into_iter()
179                .map(str::trim)
180                .filter(|s| !s.is_empty())
181                .map(str::to_owned),
182        );
183        let joined = parts.join(" ");
184        if let Ok(value) = HeaderValue::from_str(&joined) {
185            self.0.insert(USER_AGENT, value);
186        }
187        self
188    }
189
190    /// Iterates over `(name, value)` pairs with values rendered as strings.
191    ///
192    /// Non-ASCII values are rendered lossily; sensitive values are not masked.
193    pub fn iter_str(&self) -> impl Iterator<Item = (&str, String)> + '_ {
194        self.0.iter().map(|(name, value)| {
195            (
196                name.as_str(),
197                String::from_utf8_lossy(value.as_bytes()).into_owned(),
198            )
199        })
200    }
201
202    /// Returns a copy with sensitive values replaced by `***`.
203    #[must_use]
204    pub fn masked(&self) -> Headers {
205        let mut masked = HeaderMap::with_capacity(self.0.len());
206        for (name, value) in &self.0 {
207            if is_sensitive(name) {
208                masked.append(name.clone(), HeaderValue::from_static(MASKED));
209            } else {
210                masked.append(name.clone(), value.clone());
211            }
212        }
213        Headers(masked)
214    }
215}
216
217fn is_sensitive(name: &HeaderName) -> bool {
218    SENSITIVE_HEADERS.contains(&name.as_str())
219}
220
221impl fmt::Debug for Headers {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        let mut map = f.debug_map();
224        for (name, value) in &self.0 {
225            if is_sensitive(name) {
226                map.entry(&name.as_str(), &MASKED);
227            } else {
228                map.entry(&name.as_str(), &String::from_utf8_lossy(value.as_bytes()));
229            }
230        }
231        map.finish()
232    }
233}
234
235impl From<HeaderMap> for Headers {
236    fn from(map: HeaderMap) -> Self {
237        Self(map)
238    }
239}
240
241impl From<Headers> for HeaderMap {
242    fn from(headers: Headers) -> Self {
243        headers.0
244    }
245}
246
247impl<'a> IntoIterator for &'a Headers {
248    type Item = (&'a HeaderName, &'a HeaderValue);
249    type IntoIter = http::header::Iter<'a, HeaderValue>;
250
251    fn into_iter(self) -> Self::IntoIter {
252        self.0.iter()
253    }
254}
255
256/// Serializes as an object of `name -> value` pairs with sensitive values
257/// masked. Repeated header names are joined with `", "`.
258impl Serialize for Headers {
259    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
260        use serde::ser::SerializeMap;
261        let mut entries: Vec<(&str, String)> = Vec::with_capacity(self.0.len());
262        for name in self.0.keys() {
263            let rendered = if is_sensitive(name) {
264                MASKED.to_owned()
265            } else {
266                self.0
267                    .get_all(name)
268                    .iter()
269                    .map(|v| String::from_utf8_lossy(v.as_bytes()).into_owned())
270                    .collect::<Vec<_>>()
271                    .join(", ")
272            };
273            entries.push((name.as_str(), rendered));
274        }
275        let mut map = serializer.serialize_map(Some(entries.len()))?;
276        for (name, value) in entries {
277            map.serialize_entry(name, &value)?;
278        }
279        map.end()
280    }
281}
282
283/// Deserializes from an object of `name -> value` pairs; invalid entries are
284/// rejected with an error.
285impl<'de> Deserialize<'de> for Headers {
286    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
287        struct HeadersVisitor;
288
289        impl<'de> serde::de::Visitor<'de> for HeadersVisitor {
290            type Value = Headers;
291
292            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293                f.write_str("a map of header names to string values")
294            }
295
296            fn visit_map<A: serde::de::MapAccess<'de>>(
297                self,
298                mut map: A,
299            ) -> Result<Headers, A::Error> {
300                let mut headers = Headers::new();
301                while let Some((name, value)) = map.next_entry::<String, String>()? {
302                    headers
303                        .insert(&name, &value)
304                        .map_err(serde::de::Error::custom)?;
305                }
306                Ok(headers)
307            }
308        }
309
310        deserializer.deserialize_map(HeadersVisitor)
311    }
312}
313
314/// Error returned when a header name or value is invalid.
315#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
316#[error("invalid header `{name}`")]
317pub struct InvalidHeader {
318    /// The offending header name.
319    pub name: String,
320}