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 {}