http_extract/header.rs
1//! Strict building blocks for HTTP field maps.
2//!
3//! These helpers implement the field-line handling used by the higher-level
4//! extractors. They deliberately do not combine repeated lines unless the
5//! field-specific extractor owns that grammar. See [RFC 9110, Section 5]
6//! for HTTP field semantics.
7//!
8//! [RFC 9110, Section 5]: https://www.rfc-editor.org/rfc/rfc9110.html#section-5
9
10use http::{HeaderMap, HeaderName, HeaderValue};
11
12use crate::Error;
13
14/// Append one already validated field value without replacing existing values.
15///
16/// Existing values for `name` remain in the map and `value` is appended after
17/// them. The return value is `true` when the map already contained at least one
18/// value for `name`, and `false` when this call inserted its first value.
19/// [`HeaderName`] and [`HeaderValue`] validate their own inputs before this
20/// function is called, so appending is infallible. The helper does not inspect,
21/// format, or log the value; callers must continue to treat credential-bearing
22/// values as sensitive.
23pub fn append_header_value(headers: &mut HeaderMap, name: HeaderName, value: HeaderValue) -> bool {
24 headers.append(name, value)
25}
26
27/// Extract a field value only when the field has at most one field line.
28///
29/// A missing field returns `None`. More than one field line returns
30/// [`Error::DuplicateHeader`]. The returned [`HeaderValue`] is otherwise raw:
31/// this function performs no text decoding, syntax validation, authentication,
32/// or logging. It does not combine repeated lines or split values on commas;
33/// those operations are only valid for fields whose own grammar permits them.
34pub fn extract_single_header_value<'a>(
35 headers: &'a HeaderMap,
36 name: &HeaderName,
37) -> Result<Option<&'a HeaderValue>, Error> {
38 let mut values = headers.get_all(name).iter();
39 let first = values.next();
40 if values.next().is_some() {
41 return Err(Error::duplicate_header(name.clone()));
42 }
43 Ok(first)
44}
45
46/// Extract a singular field as text without silently discarding invalid bytes.
47///
48/// A missing field returns `None`, duplicate field lines return
49/// [`Error::DuplicateHeader`], and a value that cannot be represented as text
50/// returns [`Error::InvalidHeader`]. Empty text is preserved as `Some("")`.
51/// No field-specific syntax validation is performed. Returned text may contain
52/// credentials or other sensitive data and must not be logged or echoed; errors
53/// identify only the field name and error category, never the value.
54pub fn extract_single_header_text<'a>(
55 headers: &'a HeaderMap,
56 name: &HeaderName,
57) -> Result<Option<&'a str>, Error> {
58 extract_single_header_value(headers, name)?
59 .map(|value| {
60 value
61 .to_str()
62 .map_err(|_| Error::invalid_header(name.clone()))
63 })
64 .transpose()
65}
66
67#[cfg(test)]
68mod tests {
69 use http::{HeaderMap, HeaderValue, header::USER_AGENT};
70
71 use super::*;
72
73 #[test]
74 fn distinguishes_missing_invalid_and_duplicate() {
75 let mut headers = HeaderMap::new();
76 assert_eq!(extract_single_header_text(&headers, &USER_AGENT), Ok(None));
77
78 headers.insert(USER_AGENT, HeaderValue::from_bytes(&[0xff]).unwrap());
79 assert!(matches!(
80 extract_single_header_text(&headers, &USER_AGENT),
81 Err(Error::InvalidHeader { .. })
82 ));
83
84 headers.clear();
85 headers.append(USER_AGENT, HeaderValue::from_static("one"));
86 headers.append(USER_AGENT, HeaderValue::from_static("two"));
87 assert!(matches!(
88 extract_single_header_text(&headers, &USER_AGENT),
89 Err(Error::DuplicateHeader { .. })
90 ));
91 }
92
93 #[test]
94 fn append_preserves_existing_values() {
95 let mut headers = HeaderMap::new();
96 let name = HeaderName::from_static("x-example");
97 assert!(!append_header_value(
98 &mut headers,
99 name.clone(),
100 HeaderValue::from_static("first"),
101 ));
102 assert!(append_header_value(
103 &mut headers,
104 name.clone(),
105 HeaderValue::from_static("second"),
106 ));
107 assert_eq!(
108 headers
109 .get_all(name)
110 .iter()
111 .map(HeaderValue::to_str)
112 .collect::<Result<Vec<_>, _>>()
113 .unwrap(),
114 vec!["first", "second"]
115 );
116 }
117}