Skip to main content

reliar_core/
headers.rs

1//! Validating custom-header newtype (ADR 0004, ADR 0011).
2
3use core::fmt;
4use std::collections::HashMap;
5
6use crate::ids::contains_control_char;
7
8/// Application-defined metadata Reliar does not understand: a validating newtype, never a
9/// `HashMap` alias and never exposed through `Deref` (ADR 0011). Reserves the entire `reliar-`
10/// prefix (case-insensitive) so framework metadata is never duplicated here — see
11/// [`Metadata`](crate::Metadata) for the one canonical source of truth (ADR 0004).
12///
13/// ```
14/// use reliar_core::Headers;
15///
16/// let mut headers = Headers::default();
17/// headers.insert("x-import-batch", "2026-09-04")?;
18///
19/// assert_eq!(headers.get("x-import-batch"), Some("2026-09-04"));
20/// // The entire `reliar-` prefix is reserved for framework metadata, case-insensitively.
21/// assert!(headers.insert("Reliar-Correlation-Id", "nope").is_err());
22/// # Ok::<(), reliar_core::HeaderError>(())
23/// ```
24#[derive(Clone, Default, PartialEq, Eq)]
25pub struct Headers(HashMap<String, String>);
26
27/// **Never derived.** Header values are application-supplied and may themselves be secrets:
28/// every value prints as `<redacted>`, keys print verbatim so the shape of a header set is
29/// still debuggable.
30impl fmt::Debug for Headers {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        f.debug_map()
33            .entries(self.0.keys().map(|k| (k, "<redacted>")))
34            .finish()
35    }
36}
37
38impl Headers {
39    /// Case-insensitively reserved prefix; [`Self::insert`] rejects any key starting with it.
40    pub const RESERVED_PREFIX: &'static str = "reliar-";
41    /// Maximum key length in bytes.
42    pub const MAX_KEY_LEN: usize = 128;
43    /// Maximum value length in bytes.
44    pub const MAX_VALUE_LEN: usize = 1024;
45    /// Maximum number of distinct headers on one envelope.
46    pub const MAX_COUNT: usize = 32;
47
48    /// Inserts a header, returning the previous value if the key was already present.
49    ///
50    /// Returns `Err` — never a silent drop or overwrite — for: the reserved `reliar-` prefix
51    /// (matched case-insensitively, so `Reliar-Correlation-Id` is rejected too), an empty key, a
52    /// key or value containing a control character (including CR/LF — a header-injection surface
53    /// once a mapper writes the value onto the wire, same rule as [`crate::CorrelationId`],
54    /// [`crate::EndpointAddress`] and [`crate::ContentType`]), a key over [`Self::MAX_KEY_LEN`],
55    /// or a value over [`Self::MAX_VALUE_LEN`]. Replacing a key that is already present never
56    /// counts against [`Self::MAX_COUNT`]; adding a genuinely new key while already at the cap
57    /// does.
58    ///
59    /// # Errors
60    ///
61    /// Returns [`HeaderError`] for a reserved, empty, control-character-containing, or
62    /// over-length key, a control-character-containing or over-length value, or a new key that
63    /// would exceed [`Self::MAX_COUNT`].
64    ///
65    /// ```
66    /// use reliar_core::Headers;
67    ///
68    /// let mut headers = Headers::default();
69    /// assert_eq!(headers.insert("x-a", "1")?, None);
70    /// assert_eq!(headers.insert("x-a", "2")?, Some("1".to_string()));
71    /// # Ok::<(), reliar_core::HeaderError>(())
72    /// ```
73    pub fn insert(
74        &mut self,
75        key: impl Into<String>,
76        value: impl Into<String>,
77    ) -> Result<Option<String>, HeaderError> {
78        let key = key.into();
79        let value = value.into();
80
81        if Self::has_reserved_prefix(&key) {
82            return Err(HeaderError::Reserved { key });
83        }
84
85        if key.is_empty() {
86            return Err(HeaderError::EmptyKey);
87        }
88
89        if contains_control_char(&key) {
90            return Err(HeaderError::ControlCharacterInKey { key });
91        }
92
93        if key.len() > Self::MAX_KEY_LEN {
94            return Err(HeaderError::KeyTooLong { len: key.len() });
95        }
96
97        if contains_control_char(&value) {
98            return Err(HeaderError::ControlCharacterInValue { key });
99        }
100
101        if value.len() > Self::MAX_VALUE_LEN {
102            return Err(HeaderError::ValueTooLong { len: value.len() });
103        }
104
105        if !self.0.contains_key(&key) && self.0.len() >= Self::MAX_COUNT {
106            return Err(HeaderError::TooManyHeaders {
107                limit: Self::MAX_COUNT,
108            });
109        }
110
111        Ok(self.0.insert(key, value))
112    }
113
114    /// Looks up a header by exact key.
115    ///
116    /// ```
117    /// use reliar_core::Headers;
118    ///
119    /// let mut headers = Headers::default();
120    /// headers.insert("x-a", "1")?;
121    /// assert_eq!(headers.get("x-a"), Some("1"));
122    /// assert_eq!(headers.get("x-missing"), None);
123    /// # Ok::<(), reliar_core::HeaderError>(())
124    /// ```
125    #[must_use]
126    pub fn get(&self, key: &str) -> Option<&str> {
127        self.0.get(key).map(String::as_str)
128    }
129
130    /// Removes a header, returning its value if present.
131    ///
132    /// ```
133    /// use reliar_core::Headers;
134    ///
135    /// let mut headers = Headers::default();
136    /// headers.insert("x-a", "1")?;
137    /// assert_eq!(headers.remove("x-a"), Some("1".to_string()));
138    /// assert_eq!(headers.remove("x-a"), None);
139    /// # Ok::<(), reliar_core::HeaderError>(())
140    /// ```
141    pub fn remove(&mut self, key: &str) -> Option<String> {
142        self.0.remove(key)
143    }
144
145    /// The number of headers stored.
146    ///
147    /// ```
148    /// use reliar_core::Headers;
149    ///
150    /// let mut headers = Headers::default();
151    /// assert_eq!(headers.len(), 0);
152    /// headers.insert("x-a", "1")?;
153    /// assert_eq!(headers.len(), 1);
154    /// # Ok::<(), reliar_core::HeaderError>(())
155    /// ```
156    #[must_use]
157    pub fn len(&self) -> usize {
158        self.0.len()
159    }
160
161    /// Returns `true` if no headers are stored.
162    ///
163    /// ```
164    /// use reliar_core::Headers;
165    ///
166    /// let mut headers = Headers::default();
167    /// assert!(headers.is_empty());
168    /// headers.insert("x-a", "1")?;
169    /// assert!(!headers.is_empty());
170    /// # Ok::<(), reliar_core::HeaderError>(())
171    /// ```
172    #[must_use]
173    pub fn is_empty(&self) -> bool {
174        self.0.is_empty()
175    }
176
177    /// Iterates over every stored header as borrowed string slices.
178    ///
179    /// ```
180    /// use reliar_core::Headers;
181    ///
182    /// let mut headers = Headers::default();
183    /// headers.insert("x-a", "1")?;
184    /// let collected: Vec<_> = headers.iter().collect();
185    /// assert_eq!(collected, vec![("x-a", "1")]);
186    /// # Ok::<(), reliar_core::HeaderError>(())
187    /// ```
188    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
189        self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
190    }
191
192    fn has_reserved_prefix(key: &str) -> bool {
193        key.len() >= Self::RESERVED_PREFIX.len()
194            && key.as_bytes()[..Self::RESERVED_PREFIX.len()]
195                .eq_ignore_ascii_case(Self::RESERVED_PREFIX.as_bytes())
196    }
197}
198
199/// [`Headers::insert`] failures.
200///
201/// ```
202/// use reliar_core::{HeaderError, Headers};
203///
204/// let mut headers = Headers::default();
205/// let err = headers.insert("reliar-anything", "nope").unwrap_err();
206/// assert!(matches!(err, HeaderError::Reserved { .. }));
207/// ```
208#[derive(Debug, Clone, PartialEq, Eq)]
209#[non_exhaustive]
210pub enum HeaderError {
211    /// The key starts with the reserved `reliar-` prefix (case-insensitive).
212    Reserved {
213        /// The rejected key.
214        key: String,
215    },
216
217    /// The key was empty.
218    EmptyKey,
219
220    /// The key contained a control character (including CR/LF — a header-injection surface).
221    ControlCharacterInKey {
222        /// The rejected key.
223        key: String,
224    },
225
226    /// The value for this key contained a control character (including CR/LF). The value
227    /// itself is never carried on the error — it may be a secret; the key is, so the caller can
228    /// tell which header was rejected.
229    ControlCharacterInValue {
230        /// The key whose value was rejected.
231        key: String,
232    },
233
234    /// The key exceeded [`Headers::MAX_KEY_LEN`].
235    KeyTooLong {
236        /// The key's actual length in bytes.
237        len: usize,
238    },
239
240    /// The value exceeded [`Headers::MAX_VALUE_LEN`].
241    ValueTooLong {
242        /// The value's actual length in bytes.
243        len: usize,
244    },
245
246    /// Inserting a new key would exceed [`Headers::MAX_COUNT`].
247    TooManyHeaders {
248        /// The configured limit.
249        limit: usize,
250    },
251}
252
253impl fmt::Display for HeaderError {
254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255        match self {
256            Self::Reserved { key } => {
257                write!(f, "header key {key:?} uses the reserved `reliar-` prefix")
258            }
259            Self::EmptyKey => f.write_str("header key must not be empty"),
260            Self::ControlCharacterInKey { key } => {
261                write!(f, "header key {key:?} contains a control character")
262            }
263            Self::ControlCharacterInValue { key } => {
264                write!(
265                    f,
266                    "header value for key {key:?} contains a control character"
267                )
268            }
269            Self::KeyTooLong { len } => write!(
270                f,
271                "header key length {len} exceeds the maximum of {}",
272                Headers::MAX_KEY_LEN
273            ),
274            Self::ValueTooLong { len } => write!(
275                f,
276                "header value length {len} exceeds the maximum of {}",
277                Headers::MAX_VALUE_LEN
278            ),
279            Self::TooManyHeaders { limit } => {
280                write!(f, "header count would exceed the maximum of {limit}")
281            }
282        }
283    }
284}
285
286impl std::error::Error for HeaderError {}