Skip to main content

aws_smithy_runtime_api/http/
non_utf8.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Handling for response header values that are not valid UTF-8
7
8use aws_smithy_types::config_bag::{Storable, StoreReplace};
9
10/// What to do when a response header value bound to a modeled member is not valid UTF-8
11///
12/// An HTTP header value may contain any octet in `0x80..=0xFF` (obs-text, RFC 7230), and an
13/// arbitrary sequence of those is not necessarily valid UTF-8, so a service may send a value that
14/// cannot be represented as a Rust `String`. [`Headers`](crate::http::Headers) stores
15/// such a value as received, but the modeled member it is bound to is a `String`, so something has
16/// to give when the member is deserialized.
17///
18/// The default is [`Reject`](Self::Reject). To choose otherwise, put this in the config bag from an
19/// interceptor that runs before deserialization. [`Skip`](Self::Skip) does not remove the header, so
20/// the same interceptor can read the octets from any hook that sees the response, using
21/// [`Headers::get_bytes`](crate::http::Headers::get_bytes) or
22/// [`iter_bytes`](crate::http::Headers::iter_bytes):
23///
24#[cfg_attr(
25    feature = "client",
26    doc = r#"
27```no_run
28# use aws_smithy_runtime_api::box_error::BoxError;
29# use aws_smithy_runtime_api::client::interceptors::context::{
30#     BeforeDeserializationInterceptorContextRef, BeforeSerializationInterceptorContextRef,
31# };
32# use aws_smithy_runtime_api::client::interceptors::Intercept;
33# use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
34# use aws_smithy_runtime_api::http::NonUtf8HeaderHandling;
35# use aws_smithy_types::config_bag::ConfigBag;
36# use std::sync::{Arc, Mutex};
37#[derive(Clone, Debug, Default)]
38struct SkipNonUtf8Headers {
39    seen: Arc<Mutex<Vec<(String, Vec<u8>)>>>,
40}
41
42impl Intercept for SkipNonUtf8Headers {
43    fn name(&self) -> &'static str {
44        "SkipNonUtf8Headers"
45    }
46
47    fn read_before_execution(
48        &self,
49        _context: &BeforeSerializationInterceptorContextRef<'_>,
50        cfg: &mut ConfigBag,
51    ) -> Result<(), BoxError> {
52        cfg.interceptor_state()
53            .store_put(NonUtf8HeaderHandling::Skip);
54        Ok(())
55    }
56
57    fn read_before_deserialization(
58        &self,
59        context: &BeforeDeserializationInterceptorContextRef<'_>,
60        _runtime_components: &RuntimeComponents,
61        _cfg: &mut ConfigBag,
62    ) -> Result<(), BoxError> {
63        // Runs once per attempt, so overwrite rather than append.
64        *self.seen.lock().unwrap() = context
65            .response()
66            .headers()
67            .iter_bytes()
68            .filter(|(_, value)| std::str::from_utf8(value).is_err())
69            .map(|(name, value)| (name.to_owned(), value.to_vec()))
70            .collect();
71        Ok(())
72    }
73}
74```
75"#
76)]
77///
78/// This applies only to values bound to a modeled member. A header bound to nothing is never an
79/// error regardless of encoding.
80#[derive(Debug, Clone, PartialEq, Eq, Default)]
81#[non_exhaustive]
82pub enum NonUtf8HeaderHandling {
83    /// Fail the operation, reporting the member and header that could not be parsed.
84    ///
85    /// This is the default: a value the service sent is not silently discarded.
86    #[default]
87    Reject,
88
89    /// Deserialize the member as if the header were absent.
90    ///
91    /// The header itself is left in place, so the octets stay readable through
92    /// [`Headers::get_bytes`](crate::http::Headers::get_bytes).
93    ///
94    /// Note this drops the whole member, not just the offending value: for a member bound to a
95    /// list-valued header, one unreadable value makes the entire member `None`.
96    Skip,
97}
98
99impl Storable for NonUtf8HeaderHandling {
100    type Storer = StoreReplace<Self>;
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use aws_smithy_types::config_bag::{CloneableLayer, ConfigBag};
107
108    #[test]
109    fn rejects_by_default() {
110        assert_eq!(NonUtf8HeaderHandling::Reject, Default::default());
111        // An empty bag must read as `Reject` rather than requiring callers to unwrap_or_default.
112        let bag = ConfigBag::base();
113        assert_eq!(None, bag.load::<NonUtf8HeaderHandling>());
114    }
115
116    #[test]
117    fn round_trips_through_the_config_bag() {
118        let mut layer = CloneableLayer::new("test");
119        layer.store_put(NonUtf8HeaderHandling::Skip);
120        let bag = ConfigBag::of_layers(vec![layer.into()]);
121        assert_eq!(
122            Some(&NonUtf8HeaderHandling::Skip),
123            bag.load::<NonUtf8HeaderHandling>()
124        );
125    }
126}