aws_runtime/
request_info.rs1use crate::service_clock_skew::AttemptSkew;
7use aws_smithy_async::time::TimeSource;
8use aws_smithy_runtime_api::box_error::BoxError;
9use aws_smithy_runtime_api::client::interceptors::context::BeforeTransmitInterceptorContextMut;
10use aws_smithy_runtime_api::client::interceptors::{dyn_dispatch_hint, Intercept};
11use aws_smithy_runtime_api::client::retries::RequestAttempts;
12use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
13use aws_smithy_types::config_bag::ConfigBag;
14use aws_smithy_types::date_time::Format;
15use aws_smithy_types::retry::RetryConfig;
16use aws_smithy_types::timeout::TimeoutConfig;
17use aws_smithy_types::DateTime;
18use http_1x::{HeaderName, HeaderValue};
19use std::borrow::Cow;
20
21#[allow(clippy::declare_interior_mutable_const)] const AMZ_SDK_REQUEST: HeaderName = HeaderName::from_static("amz-sdk-request");
23
24#[non_exhaustive]
31#[derive(Debug, Default)]
32pub struct RequestInfoInterceptor {}
33
34impl RequestInfoInterceptor {
35 pub fn new() -> Self {
37 RequestInfoInterceptor {}
38 }
39}
40
41impl RequestInfoInterceptor {
42 fn build_attempts_pair(
43 &self,
44 cfg: &ConfigBag,
45 ) -> Option<(Cow<'static, str>, Cow<'static, str>)> {
46 let request_attempts = cfg
47 .load::<RequestAttempts>()
48 .map(|r_a| r_a.attempts())
49 .unwrap_or(0);
50 let request_attempts = request_attempts.to_string();
51 Some((Cow::Borrowed("attempt"), Cow::Owned(request_attempts)))
52 }
53
54 fn build_max_attempts_pair(
55 &self,
56 cfg: &ConfigBag,
57 ) -> Option<(Cow<'static, str>, Cow<'static, str>)> {
58 if let Some(retry_config) = cfg.load::<RetryConfig>() {
59 let max_attempts = retry_config.max_attempts().to_string();
60 Some((Cow::Borrowed("max"), Cow::Owned(max_attempts)))
61 } else {
62 None
63 }
64 }
65
66 fn build_ttl_pair(
67 &self,
68 cfg: &ConfigBag,
69 timesource: impl TimeSource,
70 ) -> Option<(Cow<'static, str>, Cow<'static, str>)> {
71 let timeout_config = cfg.load::<TimeoutConfig>()?;
72 let socket_read = timeout_config.read_timeout()?;
73 if cfg
75 .load::<RequestAttempts>()
76 .map(|a| a.attempts())
77 .unwrap_or(0)
78 < 2
79 {
80 return None;
81 }
82 let skew = cfg.load::<AttemptSkew>().map(|s| s.0).unwrap_or_default();
84 let ttl = skew.apply(timesource.now()).checked_add(socket_read)?;
85 let mut timestamp = DateTime::from(ttl);
86 timestamp.set_subsec_nanos(0);
88 let mut formatted_timestamp = timestamp
89 .fmt(Format::DateTime)
90 .expect("the resulting DateTime will always be valid");
91
92 formatted_timestamp = formatted_timestamp
94 .chars()
95 .filter(|&c| c != '-' && c != ':')
96 .collect();
97
98 Some((Cow::Borrowed("ttl"), Cow::Owned(formatted_timestamp)))
99 }
100}
101
102#[dyn_dispatch_hint]
103impl Intercept for RequestInfoInterceptor {
104 fn name(&self) -> &'static str {
105 "RequestInfoInterceptor"
106 }
107
108 fn modify_before_transmit(
109 &self,
110 context: &mut BeforeTransmitInterceptorContextMut<'_>,
111 runtime_components: &RuntimeComponents,
112 cfg: &mut ConfigBag,
113 ) -> Result<(), BoxError> {
114 let mut pairs = RequestPairs::new();
115 if let Some(pair) = self.build_ttl_pair(
116 cfg,
117 runtime_components
118 .time_source()
119 .ok_or("A timesource must be provided")?,
120 ) {
121 pairs = pairs.with_pair(pair);
122 }
123 if let Some(pair) = self.build_attempts_pair(cfg) {
124 pairs = pairs.with_pair(pair);
125 }
126 if let Some(pair) = self.build_max_attempts_pair(cfg) {
127 pairs = pairs.with_pair(pair);
128 }
129
130 let headers = context.request_mut().headers_mut();
131 headers.insert(AMZ_SDK_REQUEST, pairs.try_into_header_value()?);
132
133 Ok(())
134 }
135}
136
137#[derive(Default, Debug)]
141struct RequestPairs {
142 inner: Vec<(Cow<'static, str>, Cow<'static, str>)>,
143}
144
145impl RequestPairs {
146 fn new() -> Self {
148 Default::default()
149 }
150
151 fn with_pair(
154 mut self,
155 pair: (impl Into<Cow<'static, str>>, impl Into<Cow<'static, str>>),
156 ) -> Self {
157 let pair = (pair.0.into(), pair.1.into());
158 self.inner.push(pair);
159 self
160 }
161
162 fn try_into_header_value(self) -> Result<HeaderValue, BoxError> {
164 self.try_into()
165 }
166}
167
168impl TryFrom<RequestPairs> for HeaderValue {
169 type Error = BoxError;
170
171 fn try_from(value: RequestPairs) -> Result<Self, BoxError> {
172 let mut pairs = String::new();
173 for (key, value) in value.inner {
174 if !pairs.is_empty() {
175 pairs.push_str("; ");
176 }
177
178 pairs.push_str(&key);
179 pairs.push('=');
180 pairs.push_str(&value);
181 continue;
182 }
183 HeaderValue::from_str(&pairs).map_err(Into::into)
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::RequestInfoInterceptor;
190 use crate::request_info::RequestPairs;
191 use aws_smithy_runtime_api::client::interceptors::context::Input;
192 use aws_smithy_runtime_api::client::interceptors::context::InterceptorContext;
193 use aws_smithy_runtime_api::client::interceptors::Intercept;
194 use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
195 use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
196 use aws_smithy_types::config_bag::{ConfigBag, Layer};
197 use aws_smithy_types::retry::RetryConfig;
198 use aws_smithy_types::timeout::TimeoutConfig;
199
200 use http_1x::HeaderValue;
201 use std::time::Duration;
202
203 fn expect_header<'a>(context: &'a InterceptorContext, header_name: &str) -> &'a str {
204 context
205 .request()
206 .expect("request is set")
207 .headers()
208 .get(header_name)
209 .unwrap()
210 }
211
212 #[test]
213 fn test_request_pairs_for_initial_attempt() {
214 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
215 let mut context = InterceptorContext::new(Input::doesnt_matter());
216 context.enter_serialization_phase();
217 context.set_request(HttpRequest::empty());
218
219 let mut layer = Layer::new("test");
220 layer.store_put(RetryConfig::standard());
221 layer.store_put(
222 TimeoutConfig::builder()
223 .read_timeout(Duration::from_secs(30))
224 .build(),
225 );
226 let mut config = ConfigBag::of_layers(vec![layer]);
227
228 let _ = context.take_input();
229 context.enter_before_transmit_phase();
230 let interceptor = RequestInfoInterceptor::new();
231 let mut ctx = (&mut context).into();
232 interceptor
233 .modify_before_transmit(&mut ctx, &rc, &mut config)
234 .unwrap();
235
236 assert_eq!(
237 expect_header(&context, "amz-sdk-request"),
238 "attempt=0; max=3"
239 );
240 }
241
242 #[test]
243 fn test_header_value_from_request_pairs_supports_all_valid_characters() {
244 let rp = RequestPairs::new()
246 .with_pair(("allowed-symbols", "!#$&'*+-.^_`|~"))
247 .with_pair(("allowed-digits", "01234567890"))
248 .with_pair((
249 "allowed-characters",
250 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
251 ))
252 .with_pair(("allowed-whitespace", " \t"));
253 let _header_value: HeaderValue = rp
254 .try_into()
255 .expect("request pairs can be converted into valid header value.");
256 }
257}