Skip to main content

aws_smithy_runtime_api/http/
headers.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Types for HTTP headers
7
8use crate::http::error::{HttpError, NonUtf8Header};
9use std::borrow::Cow;
10use std::fmt::Debug;
11use std::str::FromStr;
12
13/// Header names whose values must be redacted in Debug output to prevent
14/// credential / session-token / customer-key leakage via tracing.
15const DENYLIST: &[&str] = &[
16    "authorization",
17    "proxy-authorization",
18    "x-amz-security-token",
19    "cookie",
20    "set-cookie",
21    "x-amz-server-side-encryption-customer-key",
22    "x-amz-server-side-encryption-customer-key-md5",
23    "x-amz-copy-source-server-side-encryption-customer-key",
24    "x-amz-copy-source-server-side-encryption-customer-key-md5",
25];
26
27fn is_sensitive(name: &str) -> bool {
28    DENYLIST.iter().any(|d| name.eq_ignore_ascii_case(d))
29}
30
31/// An immutable view of headers
32///
33/// Header values are stored exactly as received and are *not* required to be valid UTF-8: an HTTP
34/// header value may contain any octet in `0x80..=0xFF` (obs-text, RFC 7230), and an arbitrary
35/// sequence of those is not necessarily valid UTF-8. The string-typed accessors ([`get`](Headers::get), [`get_all`](Headers::get_all),
36/// [`iter`](Headers::iter), [`remove`](Headers::remove)) therefore yield only values that are
37/// valid UTF-8, and skip those that are not. Use the corresponding byte accessors
38/// ([`get_bytes`](Headers::get_bytes), [`get_all_bytes`](Headers::get_all_bytes),
39/// [`iter_bytes`](Headers::iter_bytes)) to observe every value.
40///
41/// Consequently [`len`](Headers::len) and [`contains_key`](Headers::contains_key) count and
42/// report values the string accessors skip.
43#[derive(Clone, Default)]
44pub struct Headers {
45    pub(super) headers: http_1x::HeaderMap<HeaderValue>,
46}
47
48impl Debug for Headers {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        let mut map = f.debug_map();
51        for (key, value) in self.headers.iter() {
52            let name = key.as_str();
53            if is_sensitive(name) {
54                map.entry(
55                    &name,
56                    &format_args!("** redacted (length={}) **", value.as_bytes().len()),
57                );
58            } else {
59                match value.try_as_str() {
60                    Some(value) => map.entry(&name, &value),
61                    None => map.entry(
62                        &name,
63                        &format_args!("** non-utf8 (length={}) **", value.as_bytes().len()),
64                    ),
65                };
66            }
67        }
68        map.finish()
69    }
70}
71
72impl<'a> IntoIterator for &'a Headers {
73    type Item = (&'a str, &'a str);
74    type IntoIter = HeadersIter<'a>;
75
76    fn into_iter(self) -> Self::IntoIter {
77        HeadersIter {
78            inner: self.headers.iter(),
79        }
80    }
81}
82
83/// An Iterator over headers
84pub struct HeadersIter<'a> {
85    inner: http_1x::header::Iter<'a, HeaderValue>,
86}
87
88impl<'a> Iterator for HeadersIter<'a> {
89    type Item = (&'a str, &'a str);
90
91    fn next(&mut self) -> Option<Self::Item> {
92        // Values that are not valid UTF-8 are skipped; use `Headers::iter_bytes` to see them.
93        loop {
94            let (name, value) = self.inner.next()?;
95            if let Some(value) = value.try_as_str() {
96                return Some((name.as_str(), value));
97            }
98        }
99    }
100}
101
102impl Headers {
103    /// Create an empty header map
104    pub fn new() -> Self {
105        Self::default()
106    }
107
108    #[cfg(feature = "http-1x")]
109    pub(crate) fn http1_headermap(self) -> http_1x::HeaderMap {
110        let mut headers = http_1x::HeaderMap::new();
111        headers.reserve(self.headers.len());
112        headers.extend(self.headers.into_iter().map(|(k, v)| (k, v.into_http1x())));
113        headers
114    }
115
116    #[cfg(feature = "http-02x")]
117    pub(crate) fn http0_headermap(self) -> http_02x::HeaderMap {
118        let mut headers = http_02x::HeaderMap::new();
119        headers.reserve(self.headers.len());
120        headers.extend(self.headers.into_iter().map(|(k, v)| {
121            (
122                k.map(|n| {
123                    http_02x::HeaderName::from_bytes(n.as_str().as_bytes()).expect("proven valid")
124                }),
125                v.into_http02x(),
126            )
127        }));
128        headers
129    }
130
131    /// Returns the value for a given key
132    ///
133    /// Returns `None` if the header is absent, or if its value is not valid UTF-8; use
134    /// [`get_bytes`](Self::get_bytes) to read a value of any encoding.
135    ///
136    /// If multiple values are associated, the first value is returned
137    /// See [HeaderMap::get](http_1x::HeaderMap::get)
138    pub fn get(&self, key: impl AsRef<str>) -> Option<&str> {
139        self.headers.get(key.as_ref()).and_then(|v| v.try_as_str())
140    }
141
142    /// Returns the value for a given key, distinguishing an unreadable value from an absent header
143    ///
144    /// `Some(Ok(_))` is a value that is valid UTF-8, `Some(Err(_))` is the raw octets of one that is
145    /// not, and `None` means the header is absent. [`get`](Self::get) collapses the first two into
146    /// `None`, so use this where the difference matters.
147    ///
148    /// If multiple values are associated, the first value is returned.
149    pub fn try_get(&self, key: impl AsRef<str>) -> Option<Result<&str, &[u8]>> {
150        self.headers
151            .get(key.as_ref())
152            .map(|value| match value.try_as_str() {
153                Some(value) => Ok(value),
154                None => Err(value.as_bytes()),
155            })
156    }
157
158    /// Returns all values for a given key
159    ///
160    /// Values that are not valid UTF-8 are skipped; use
161    /// [`get_all_bytes`](Self::get_all_bytes) to read values of any encoding.
162    pub fn get_all(&self, key: impl AsRef<str>) -> impl Iterator<Item = &str> {
163        self.headers
164            .get_all(key.as_ref())
165            .iter()
166            .filter_map(|v| v.try_as_str())
167    }
168
169    /// Returns the value for a given key as raw bytes
170    ///
171    /// Unlike [`get`](Self::get), the returned bytes are not required to be valid UTF-8.
172    ///
173    /// If multiple values are associated, the first value is returned.
174    pub fn get_bytes(&self, key: impl AsRef<str>) -> Option<&[u8]> {
175        self.headers.get(key.as_ref()).map(|v| v.as_bytes())
176    }
177
178    /// Returns all values for a given key as raw bytes
179    ///
180    /// Unlike [`get_all`](Self::get_all), the returned bytes are not required to be valid UTF-8.
181    pub fn get_all_bytes(&self, key: impl AsRef<str>) -> impl Iterator<Item = &[u8]> {
182        self.headers
183            .get_all(key.as_ref())
184            .iter()
185            .map(|v| v.as_bytes())
186    }
187
188    /// Returns an iterator over the headers
189    pub fn iter(&self) -> HeadersIter<'_> {
190        HeadersIter {
191            inner: self.headers.iter(),
192        }
193    }
194
195    /// Returns an iterator over the headers, pairing each name with its raw value bytes
196    ///
197    /// Unlike [`iter`](Self::iter), the returned bytes are not required to be valid UTF-8.
198    pub fn iter_bytes(&self) -> impl Iterator<Item = (&str, &[u8])> {
199        self.headers.iter().map(|(k, v)| (k.as_str(), v.as_bytes()))
200    }
201
202    /// Returns the total number of **values** stored in the map
203    pub fn len(&self) -> usize {
204        self.headers.len()
205    }
206
207    /// Returns true if there are no headers
208    pub fn is_empty(&self) -> bool {
209        self.len() == 0
210    }
211
212    /// Returns true if this header is present
213    pub fn contains_key(&self, key: impl AsRef<str>) -> bool {
214        self.headers.contains_key(key.as_ref())
215    }
216
217    /// Insert a value into the headers structure.
218    ///
219    /// This will *replace* any existing value for this key. Returns the previous associated value if any.
220    ///
221    /// # Panics
222    /// If the key is not valid ASCII, or if the value is not valid UTF-8, this function will panic.
223    pub fn insert(
224        &mut self,
225        key: impl AsHeaderComponent,
226        value: impl AsHeaderComponent,
227    ) -> Option<String> {
228        let key = header_name(key, false).unwrap();
229        let value = header_value(value.into_maybe_static().unwrap(), false).unwrap();
230        self.headers
231            .insert(key, value)
232            .and_then(|old_value| old_value.try_as_str().map(str::to_string))
233    }
234
235    /// Insert a value into the headers structure.
236    ///
237    /// This will *replace* any existing value for this key. Returns the previous associated value if any.
238    ///
239    /// If the key is not valid ASCII, or if the value is not valid UTF-8, this function will return an error.
240    pub fn try_insert(
241        &mut self,
242        key: impl AsHeaderComponent,
243        value: impl AsHeaderComponent,
244    ) -> Result<Option<String>, HttpError> {
245        let key = header_name(key, true)?;
246        let value = header_value(value.into_maybe_static()?, true)?;
247        Ok(self
248            .headers
249            .insert(key, value)
250            .and_then(|old_value| old_value.try_as_str().map(str::to_string)))
251    }
252
253    /// Appends a value to a given key
254    ///
255    /// # Panics
256    /// If the key is not valid ASCII, or if the value is not valid UTF-8, this function will panic.
257    pub fn append(&mut self, key: impl AsHeaderComponent, value: impl AsHeaderComponent) -> bool {
258        let key = header_name(key.into_maybe_static().unwrap(), false).unwrap();
259        let value = header_value(value.into_maybe_static().unwrap(), false).unwrap();
260        self.headers.append(key, value)
261    }
262
263    /// Appends a value to a given key
264    ///
265    /// If the key is not valid ASCII, or if the value is not valid UTF-8, this function will return an error.
266    pub fn try_append(
267        &mut self,
268        key: impl AsHeaderComponent,
269        value: impl AsHeaderComponent,
270    ) -> Result<bool, HttpError> {
271        let key = header_name(key.into_maybe_static()?, true)?;
272        let value = header_value(value.into_maybe_static()?, true)?;
273        Ok(self.headers.append(key, value))
274    }
275
276    /// Removes all headers with a given key
277    ///
278    /// If there are multiple entries for this key, the first entry is returned. Returns `None`
279    /// if the first value is not valid UTF-8; the headers are removed either way.
280    pub fn remove(&mut self, key: impl AsRef<str>) -> Option<String> {
281        self.headers
282            .remove(key.as_ref())
283            .and_then(|h| h.try_as_str().map(str::to_string))
284    }
285}
286
287#[cfg(feature = "http-02x")]
288impl TryFrom<http_02x::HeaderMap> for Headers {
289    type Error = HttpError;
290
291    fn try_from(value: http_02x::HeaderMap) -> Result<Self, Self::Error> {
292        // Values are admitted regardless of encoding; see `Headers` for how non-UTF-8 values
293        // surface to readers.
294        //
295        // `http` 0.2.x accepts some header names that `http` 1.x rejects (for example names
296        // containing `"`). Convert fallibly and surface an error instead of panicking.
297        //
298        // A `None` key in `HeaderMap`'s iterator means "same name as the previous entry"
299        // (multi-value headers), so the converted names are collected in order before being
300        // extended into the map to preserve that association.
301        let converted: Vec<(Option<http_1x::HeaderName>, HeaderValue)> = value
302            .into_iter()
303            .map(|(k, v)| {
304                let name = k
305                    .map(|n| http_1x::HeaderName::from_bytes(n.as_str().as_bytes()))
306                    .transpose()
307                    .map_err(HttpError::invalid_header_name)?;
308                Ok((name, HeaderValue::from_http02x(v)))
309            })
310            .collect::<Result<_, HttpError>>()?;
311        let mut headers: http_1x::HeaderMap<HeaderValue> = Default::default();
312        headers.extend(converted);
313        Ok(Headers { headers })
314    }
315}
316
317#[cfg(feature = "http-1x")]
318impl TryFrom<http_1x::HeaderMap> for Headers {
319    type Error = HttpError;
320
321    fn try_from(value: http_1x::HeaderMap) -> Result<Self, Self::Error> {
322        // Values are admitted regardless of encoding; see `Headers` for how non-UTF-8 values
323        // surface to readers. This conversion is infallible, but the signature is retained
324        // because header names may be rejected by the `http` 0.2.x conversion above.
325        let mut headers: http_1x::HeaderMap<HeaderValue> = Default::default();
326        headers.extend(
327            value
328                .into_iter()
329                .map(|(k, v)| (k, HeaderValue::from_http1x(v))),
330        );
331        Ok(Headers { headers })
332    }
333}
334
335use sealed::AsHeaderComponent;
336
337mod sealed {
338    use super::*;
339    /// Trait defining things that may be converted into a header component (name or value)
340    pub trait AsHeaderComponent {
341        /// If the component can be represented as a Cow<'static, str>, return it
342        fn into_maybe_static(self) -> Result<MaybeStatic, HttpError>;
343
344        /// Return a string reference to this header
345        fn as_str(&self) -> Result<&str, HttpError>;
346
347        /// If a component is already internally represented as a `http_1x::HeaderName`, return it
348        fn repr_as_http1x_header_name(self) -> Result<http_1x::HeaderName, Self>
349        where
350            Self: Sized,
351        {
352            Err(self)
353        }
354    }
355
356    impl AsHeaderComponent for &'static str {
357        fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
358            Ok(Cow::Borrowed(self))
359        }
360
361        fn as_str(&self) -> Result<&str, HttpError> {
362            Ok(self)
363        }
364    }
365
366    impl AsHeaderComponent for String {
367        fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
368            Ok(Cow::Owned(self))
369        }
370
371        fn as_str(&self) -> Result<&str, HttpError> {
372            Ok(self)
373        }
374    }
375
376    impl AsHeaderComponent for Cow<'static, str> {
377        fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
378            Ok(self)
379        }
380
381        fn as_str(&self) -> Result<&str, HttpError> {
382            Ok(self.as_ref())
383        }
384    }
385
386    #[cfg(feature = "http-02x")]
387    impl AsHeaderComponent for http_02x::HeaderValue {
388        fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
389            Ok(Cow::Owned(
390                std::str::from_utf8(self.as_bytes())
391                    .map_err(|err| {
392                        HttpError::non_utf8_header(NonUtf8Header::new(
393                            self.as_bytes().to_vec(),
394                            err,
395                        ))
396                    })?
397                    .to_string(),
398            ))
399        }
400
401        fn as_str(&self) -> Result<&str, HttpError> {
402            std::str::from_utf8(self.as_bytes()).map_err(|err| {
403                HttpError::non_utf8_header(NonUtf8Header::new(self.as_bytes().to_vec(), err))
404            })
405        }
406    }
407
408    #[cfg(feature = "http-02x")]
409    impl AsHeaderComponent for http_02x::HeaderName {
410        fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
411            Ok(self.to_string().into())
412        }
413
414        fn as_str(&self) -> Result<&str, HttpError> {
415            Ok(self.as_ref())
416        }
417    }
418
419    impl AsHeaderComponent for http_1x::HeaderName {
420        fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
421            Ok(self.to_string().into())
422        }
423
424        fn as_str(&self) -> Result<&str, HttpError> {
425            Ok(self.as_ref())
426        }
427
428        fn repr_as_http1x_header_name(self) -> Result<http_1x::HeaderName, Self>
429        where
430            Self: Sized,
431        {
432            Ok(self)
433        }
434    }
435
436    impl AsHeaderComponent for http_1x::HeaderValue {
437        fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
438            Ok(Cow::Owned(
439                std::str::from_utf8(self.as_bytes())
440                    .map_err(|err| {
441                        HttpError::non_utf8_header(NonUtf8Header::new(
442                            self.as_bytes().to_vec(),
443                            err,
444                        ))
445                    })?
446                    .to_string(),
447            ))
448        }
449
450        fn as_str(&self) -> Result<&str, HttpError> {
451            std::str::from_utf8(self.as_bytes()).map_err(|err| {
452                HttpError::non_utf8_header(NonUtf8Header::new(self.as_bytes().to_vec(), err))
453            })
454        }
455    }
456}
457
458mod header_value {
459    use super::*;
460
461    /// HeaderValue type
462    ///
463    /// **Note**: Unlike `HeaderValue` in `http`, this only supports UTF-8 header values
464    #[derive(Debug, Clone)]
465    pub struct HeaderValue {
466        _private: Inner,
467    }
468
469    #[derive(Debug, Clone)]
470    enum Inner {
471        #[cfg(feature = "http-02x")]
472        H0(http_02x::HeaderValue),
473        H1(http_1x::HeaderValue),
474    }
475
476    impl HeaderValue {
477        // Encoding is not validated here. Callers that require UTF-8 go through
478        // `AsHeaderComponent`, which validates before reaching this point.
479        #[cfg(feature = "http-02x")]
480        pub(crate) fn from_http02x(value: http_02x::HeaderValue) -> Self {
481            Self {
482                _private: Inner::H0(value),
483            }
484        }
485
486        // Encoding is not validated here; see `from_http02x`.
487        pub(crate) fn from_http1x(value: http_1x::HeaderValue) -> Self {
488            Self {
489                _private: Inner::H1(value),
490            }
491        }
492
493        #[cfg(feature = "http-02x")]
494        pub(crate) fn into_http02x(self) -> http_02x::HeaderValue {
495            match self._private {
496                Inner::H0(v) => v,
497                Inner::H1(v) => http_02x::HeaderValue::from_maybe_shared(v).expect("unreachable"),
498            }
499        }
500
501        #[allow(dead_code)]
502        pub(crate) fn into_http1x(self) -> http_1x::HeaderValue {
503            match self._private {
504                Inner::H1(v) => v,
505                #[cfg(feature = "http-02x")]
506                Inner::H0(v) => http_1x::HeaderValue::from_maybe_shared(v).expect("unreachable"),
507            }
508        }
509    }
510
511    impl AsRef<str> for HeaderValue {
512        /// # Panics
513        /// If the value is not valid UTF-8. See [`HeaderValue::as_str`].
514        fn as_ref(&self) -> &str {
515            std::str::from_utf8(self.as_bytes()).expect("header value is not valid UTF-8")
516        }
517    }
518
519    impl From<HeaderValue> for String {
520        fn from(value: HeaderValue) -> Self {
521            value.as_ref().to_string()
522        }
523    }
524
525    impl HeaderValue {
526        /// Returns the string representation of this header value
527        ///
528        /// # Panics
529        /// If the value is not valid UTF-8. A `HeaderValue` stored in a [`Headers`] may be of any
530        /// encoding, so prefer [`try_as_str`](Self::try_as_str) or
531        /// [`as_bytes`](Self::as_bytes) unless the value is one you constructed yourself, which
532        /// is necessarily valid UTF-8 because the only public constructors ([`FromStr`] and
533        /// [`TryFrom<String>`]) take a `str`.
534        ///
535        /// No accessor on [`Headers`] hands out a `HeaderValue`, so this is not reachable through
536        /// one. Note [`From<HeaderValue> for String`](String::from) panics for the same reason.
537        pub fn as_str(&self) -> &str {
538            self.as_ref()
539        }
540
541        /// Returns the bytes of this header value exactly as they were received
542        ///
543        /// Unlike [`as_str`](Self::as_str), this is always available.
544        pub fn as_bytes(&self) -> &[u8] {
545            match &self._private {
546                #[cfg(feature = "http-02x")]
547                Inner::H0(v) => v.as_bytes(),
548                Inner::H1(v) => v.as_bytes(),
549            }
550        }
551
552        /// Returns the string representation of this header value, or `None` if it is not
553        /// valid UTF-8
554        pub fn try_as_str(&self) -> Option<&str> {
555            std::str::from_utf8(self.as_bytes()).ok()
556        }
557    }
558
559    impl FromStr for HeaderValue {
560        type Err = HttpError;
561
562        fn from_str(s: &str) -> Result<Self, Self::Err> {
563            HeaderValue::try_from(s.to_string())
564        }
565    }
566
567    impl TryFrom<String> for HeaderValue {
568        type Error = HttpError;
569
570        fn try_from(value: String) -> Result<Self, Self::Error> {
571            Ok(HeaderValue::from_http1x(
572                http_1x::HeaderValue::try_from(value).map_err(HttpError::invalid_header_value)?,
573            ))
574        }
575    }
576}
577
578pub use header_value::HeaderValue;
579
580type MaybeStatic = Cow<'static, str>;
581
582fn header_name(
583    name: impl AsHeaderComponent,
584    panic_safe: bool,
585) -> Result<http_1x::HeaderName, HttpError> {
586    name.repr_as_http1x_header_name().or_else(|name| {
587        name.into_maybe_static().and_then(|mut cow| {
588            if cow.chars().any(|c| c.is_ascii_uppercase()) {
589                cow = Cow::Owned(cow.to_ascii_uppercase());
590            }
591            match cow {
592                Cow::Borrowed(s) if panic_safe => {
593                    http_1x::HeaderName::try_from(s).map_err(HttpError::invalid_header_name)
594                }
595                Cow::Borrowed(static_s) => Ok(http_1x::HeaderName::from_static(static_s)),
596                Cow::Owned(s) => {
597                    http_1x::HeaderName::try_from(s).map_err(HttpError::invalid_header_name)
598                }
599            }
600        })
601    })
602}
603
604fn header_value(value: MaybeStatic, panic_safe: bool) -> Result<HeaderValue, HttpError> {
605    let header = match value {
606        Cow::Borrowed(b) if panic_safe => {
607            http_1x::HeaderValue::try_from(b).map_err(HttpError::invalid_header_value)?
608        }
609        Cow::Borrowed(b) => http_1x::HeaderValue::from_static(b),
610        Cow::Owned(s) => {
611            http_1x::HeaderValue::try_from(s).map_err(HttpError::invalid_header_value)?
612        }
613    };
614    // `value` is a `Cow<'static, str>`, so the result is valid UTF-8 by construction.
615    Ok(HeaderValue::from_http1x(header))
616}
617
618#[cfg(test)]
619mod tests {
620    use super::*;
621
622    #[test]
623    fn headers_can_be_any_string() {
624        let _: HeaderValue = "😹".parse().expect("can be any string");
625        let _: HeaderValue = "abcd".parse().expect("can be any string");
626        let _ = "a\nb"
627            .parse::<HeaderValue>()
628            .expect_err("cannot contain control characters");
629    }
630
631    #[test]
632    fn no_panic_insert_upper_case_header_name() {
633        let mut headers = Headers::new();
634        headers.insert("I-Have-Upper-Case", "foo");
635    }
636    #[test]
637    fn no_panic_append_upper_case_header_name() {
638        let mut headers = Headers::new();
639        headers.append("I-Have-Upper-Case", "foo");
640    }
641
642    #[test]
643    #[should_panic]
644    fn panic_insert_invalid_ascii_key() {
645        let mut headers = Headers::new();
646        headers.insert("💩", "foo");
647    }
648    #[test]
649    #[should_panic]
650    fn panic_insert_invalid_header_value() {
651        let mut headers = Headers::new();
652        headers.insert("foo", "💩");
653    }
654    #[test]
655    #[should_panic]
656    fn panic_append_invalid_ascii_key() {
657        let mut headers = Headers::new();
658        headers.append("💩", "foo");
659    }
660    #[test]
661    #[should_panic]
662    fn panic_append_invalid_header_value() {
663        let mut headers = Headers::new();
664        headers.append("foo", "💩");
665    }
666
667    #[test]
668    fn no_panic_try_insert_invalid_ascii_key() {
669        let mut headers = Headers::new();
670        assert!(headers.try_insert("💩", "foo").is_err());
671    }
672    #[test]
673    fn no_panic_try_insert_invalid_header_value() {
674        let mut headers = Headers::new();
675        assert!(headers
676            .try_insert(
677                "foo",
678                // Valid header value with invalid UTF-8
679                http_1x::HeaderValue::from_bytes(&[0xC0, 0x80]).unwrap()
680            )
681            .is_err());
682    }
683    #[test]
684    fn no_panic_try_append_invalid_ascii_key() {
685        let mut headers = Headers::new();
686        assert!(headers.try_append("💩", "foo").is_err());
687    }
688    #[test]
689    fn no_panic_try_append_invalid_header_value() {
690        let mut headers = Headers::new();
691        assert!(headers
692            .try_append(
693                "foo",
694                // Valid header value with invalid UTF-8
695                http_1x::HeaderValue::from_bytes(&[0xC0, 0x80]).unwrap()
696            )
697            .is_err());
698    }
699
700    #[test]
701    fn header_value_exposes_bytes_and_checked_str() {
702        let value: HeaderValue = "hello".parse().expect("valid");
703        assert_eq!(b"hello", value.as_bytes());
704        assert_eq!(Some("hello"), value.try_as_str());
705        assert_eq!("hello", value.as_str());
706    }
707
708    #[cfg(feature = "http-1x")]
709    #[test]
710    fn byte_accessors_agree_with_str_accessors() {
711        let mut map = http_1x::HeaderMap::new();
712        map.append("single", http_1x::HeaderValue::from_static("v1"));
713        map.append("multi", http_1x::HeaderValue::from_static("m1"));
714        map.append("multi", http_1x::HeaderValue::from_static("m2"));
715        let headers = Headers::try_from(map).expect("all values are valid UTF-8");
716
717        assert_eq!(Some(b"v1".as_slice()), headers.get_bytes("single"));
718        assert_eq!(
719            headers.get("single").map(str::as_bytes),
720            headers.get_bytes("single")
721        );
722
723        let all_bytes: Vec<_> = headers.get_all_bytes("multi").collect();
724        assert_eq!(vec![b"m1".as_slice(), b"m2".as_slice()], all_bytes);
725        let all_str: Vec<_> = headers.get_all("multi").map(str::as_bytes).collect();
726        assert_eq!(all_str, all_bytes);
727
728        assert_eq!(None, headers.get_bytes("absent"));
729
730        let mut from_bytes: Vec<_> = headers.iter_bytes().collect();
731        from_bytes.sort();
732        let mut from_str: Vec<_> = headers.iter().map(|(k, v)| (k, v.as_bytes())).collect();
733        from_str.sort();
734        assert_eq!(from_str, from_bytes);
735    }
736
737    // Reported in review: `insert`/`try_insert` return the previous value, which reaches the
738    // panicking `AsRef<str>` when that value was admitted as non-UTF-8.
739    #[cfg(feature = "http-1x")]
740    #[test]
741    fn replacing_a_non_utf8_value_does_not_panic() {
742        for replace in [
743            (|h: &mut Headers| {
744                h.insert("bad", "replacement");
745            }) as fn(&mut Headers),
746            |h: &mut Headers| {
747                h.try_insert("bad", "replacement").expect("valid");
748            },
749        ] {
750            let mut map = http_1x::HeaderMap::new();
751            map.insert("bad", non_utf8_header_value());
752            let mut headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
753            let res =
754                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| replace(&mut headers)));
755            assert!(res.is_ok(), "replacing an admitted value must not panic");
756        }
757    }
758
759    proptest::proptest! {
760        #[test]
761        fn insert_header_prop_test(input in ".*") {
762            let mut headers = Headers::new();
763            let _ = headers.try_insert(input.clone(), input);
764        }
765
766        #[test]
767        fn append_header_prop_test(input in ".*") {
768            let mut headers = Headers::new();
769            let _ = headers.try_append(input.clone(), input);
770        }
771    }
772
773    // `http` 0.2.x accepts header names (e.g. containing `"`) that `http` 1.x rejects. Converting
774    // such a map must return an `Err`, not panic.
775    #[cfg(feature = "http-02x")]
776    #[test]
777    fn converting_an_http02x_headermap_never_panics() {
778        let name = http_02x::HeaderName::from_bytes(b"a\"b").expect("http 0.2.x accepts this");
779        let mut map = http_02x::HeaderMap::new();
780        map.insert(name, http_02x::HeaderValue::from_static("v"));
781        let res = std::panic::catch_unwind(|| Headers::try_from(map));
782        assert!(
783            res.is_ok(),
784            "TryFrom<http_02x::HeaderMap> for Headers panicked on a name that http 0.2.x \
785             considers valid but http 1.x does not; a TryFrom should return Err"
786        );
787        assert!(
788            res.unwrap().is_err(),
789            "expected an Err for a header name that http 1.x rejects"
790        );
791    }
792
793    // A lone 0xE9 is a valid HTTP header octet (obs-text per RFC 7230) but is not valid UTF-8.
794    // Every user of this fixture builds a `HeaderMap` from one of the `http` crates, so it is dead
795    // code when neither is enabled.
796    #[cfg(any(feature = "http-1x", feature = "http-02x"))]
797    const NON_UTF8_VALUE: &[u8] = b"value-\xe9";
798
799    #[cfg(feature = "http-1x")]
800    fn non_utf8_header_value() -> http_1x::HeaderValue {
801        http_1x::HeaderValue::from_bytes(NON_UTF8_VALUE).expect("valid header octets")
802    }
803
804    #[cfg(feature = "http-1x")]
805    #[test]
806    fn non_utf8_values_are_admitted_and_readable_as_bytes() {
807        let mut map = http_1x::HeaderMap::new();
808        map.insert("ok", http_1x::HeaderValue::from_static("v"));
809        map.insert("bad", non_utf8_header_value());
810        let headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
811
812        // The value is present...
813        assert!(headers.contains_key("bad"));
814        assert_eq!(Some(NON_UTF8_VALUE), headers.get_bytes("bad"));
815        // ...but is not offered as a string.
816        assert_eq!(None, headers.get("bad"));
817
818        assert_eq!(Some("v"), headers.get("ok"));
819    }
820
821    #[cfg(feature = "http-02x")]
822    #[test]
823    fn non_utf8_values_are_admitted_from_an_http02x_headermap() {
824        let mut map = http_02x::HeaderMap::new();
825        map.insert(
826            "bad",
827            http_02x::HeaderValue::from_bytes(NON_UTF8_VALUE).expect("valid header octets"),
828        );
829        let headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
830        assert_eq!(Some(NON_UTF8_VALUE), headers.get_bytes("bad"));
831        assert_eq!(None, headers.get("bad"));
832    }
833
834    #[cfg(feature = "http-1x")]
835    #[test]
836    fn a_non_utf8_value_does_not_hide_the_other_values_of_its_header() {
837        let mut map = http_1x::HeaderMap::new();
838        map.append("multi", http_1x::HeaderValue::from_static("v1"));
839        map.append("multi", non_utf8_header_value());
840        map.append("multi", http_1x::HeaderValue::from_static("v3"));
841        let headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
842
843        assert_eq!(
844            vec!["v1", "v3"],
845            headers.get_all("multi").collect::<Vec<_>>()
846        );
847        assert_eq!(
848            vec![b"v1".as_slice(), NON_UTF8_VALUE, b"v3".as_slice()],
849            headers.get_all_bytes("multi").collect::<Vec<_>>()
850        );
851        // `get` returns the first value, which here is valid UTF-8.
852        assert_eq!(Some("v1"), headers.get("multi"));
853    }
854
855    #[cfg(feature = "http-1x")]
856    #[test]
857    fn try_get_distinguishes_unreadable_from_absent() {
858        let mut map = http_1x::HeaderMap::new();
859        map.insert("ok", http_1x::HeaderValue::from_static("v"));
860        map.insert("bad", non_utf8_header_value());
861        let headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
862
863        assert_eq!(Some(Ok("v")), headers.try_get("ok"));
864        assert_eq!(Some(Err(NON_UTF8_VALUE)), headers.try_get("bad"));
865        assert_eq!(None, headers.try_get("absent"));
866
867        // `get` cannot tell the last two apart, which is why `try_get` exists.
868        assert_eq!(None, headers.get("bad"));
869        assert_eq!(None, headers.get("absent"));
870    }
871
872    #[cfg(feature = "http-1x")]
873    #[test]
874    fn iter_skips_non_utf8_values_and_iter_bytes_does_not() {
875        let mut map = http_1x::HeaderMap::new();
876        map.insert("a", non_utf8_header_value());
877        map.insert("b", http_1x::HeaderValue::from_static("v"));
878        map.insert("c", non_utf8_header_value());
879        let headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
880
881        assert_eq!(vec![("b", "v")], headers.iter().collect::<Vec<_>>());
882        assert_eq!(3, headers.iter_bytes().count());
883        // `len` counts stored values, including those `iter` skips.
884        assert_eq!(3, headers.len());
885    }
886
887    #[cfg(feature = "http-1x")]
888    #[test]
889    fn remove_drops_a_non_utf8_value_and_reports_none() {
890        let mut map = http_1x::HeaderMap::new();
891        map.insert("bad", non_utf8_header_value());
892        let mut headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
893
894        assert_eq!(None, headers.remove("bad"));
895        assert!(
896            !headers.contains_key("bad"),
897            "the header is removed regardless"
898        );
899    }
900
901    #[cfg(feature = "http-1x")]
902    #[test]
903    fn debug_marks_non_utf8_values_without_panicking() {
904        let mut map = http_1x::HeaderMap::new();
905        map.insert("bad", non_utf8_header_value());
906        let headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
907
908        let output = format!("{headers:?}");
909        assert!(output.contains("bad"), "{output}");
910        assert!(output.contains("non-utf8"), "{output}");
911    }
912
913    // Multi-value headers rely on the `None`-key semantics of `HeaderMap`'s iterator; make sure the
914    // fallible conversion preserves all values for a repeated name.
915    #[cfg(feature = "http-02x")]
916    #[test]
917    fn converting_an_http02x_headermap_preserves_multi_value_headers() {
918        let mut map = http_02x::HeaderMap::new();
919        map.append("multi", http_02x::HeaderValue::from_static("v1"));
920        map.append("multi", http_02x::HeaderValue::from_static("v2"));
921        let headers = Headers::try_from(map).expect("valid headers");
922        let values: Vec<_> = headers.get_all("multi").collect();
923        assert_eq!(values, vec!["v1", "v2"]);
924    }
925}
926
927#[cfg(test)]
928mod redaction_tests {
929    use super::*;
930
931    #[test]
932    fn debug_redacts_authorization() {
933        let mut headers = Headers::new();
934        headers.insert(
935            "authorization",
936            "AWS4-HMAC-SHA256 Credential=AKIAXXX/.../Signature=SECRETSIGMARKER",
937        );
938        let output = format!("{:?}", headers);
939        assert!(!output.contains("SECRETSIGMARKER"));
940        assert!(output.contains("authorization"));
941        assert!(output.contains("** redacted"));
942    }
943
944    #[test]
945    fn debug_redacts_security_token() {
946        let mut headers = Headers::new();
947        headers.insert("x-amz-security-token", "IQoJb3JpZ2luSECRETTOKENMARKERzzz");
948        let output = format!("{:?}", headers);
949        assert!(!output.contains("SECRETTOKENMARKER"));
950        assert!(output.contains("x-amz-security-token"));
951        assert!(output.contains("length="));
952    }
953
954    #[test]
955    fn debug_redacts_mixed_case_header_name() {
956        let mut headers = Headers::new();
957        headers.insert(
958            "Authorization",
959            "AWS4-HMAC-SHA256 Credential=AKIAXXX/.../Signature=SECRETSIGMARKER",
960        );
961        let output = format!("{:?}", headers);
962        assert!(!output.contains("SECRETSIGMARKER"));
963        assert!(output.contains("** redacted"));
964    }
965
966    #[test]
967    fn debug_preserves_non_sensitive_headers() {
968        let mut headers = Headers::new();
969        headers.insert("host", "example.com");
970        headers.insert("x-amz-user-agent", "aws-sdk-rust/1.0");
971        let output = format!("{:?}", headers);
972        assert!(output.contains("example.com"));
973        assert!(output.contains("aws-sdk-rust/1.0"));
974    }
975
976    #[test]
977    fn debug_handles_sse_customer_key() {
978        let mut headers = Headers::new();
979        headers.insert(
980            "x-amz-server-side-encryption-customer-key",
981            "BASE64KEYMARKER_DO_NOT_LOG",
982        );
983        let output = format!("{:?}", headers);
984        assert!(!output.contains("BASE64KEYMARKER_DO_NOT_LOG"));
985        assert!(output.contains("x-amz-server-side-encryption-customer-key"));
986        assert!(output.contains("** redacted"));
987    }
988
989    #[test]
990    fn debug_includes_length() {
991        let value = "exactly-twenty-chars";
992        assert_eq!(value.len(), 20);
993        let mut headers = Headers::new();
994        headers.insert("authorization", value);
995        let output = format!("{:?}", headers);
996        assert!(output.contains("length=20"));
997    }
998}