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