aws_runtime/user_agent/
interceptor.rs
1use std::borrow::Cow;
7use std::fmt;
8
9use http_02x::header::{HeaderName, HeaderValue, InvalidHeaderValue, USER_AGENT};
10
11use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature;
12use aws_smithy_runtime_api::box_error::BoxError;
13use aws_smithy_runtime_api::client::http::HttpClient;
14use aws_smithy_runtime_api::client::interceptors::context::{
15 BeforeTransmitInterceptorContextMut, BeforeTransmitInterceptorContextRef,
16};
17use aws_smithy_runtime_api::client::interceptors::Intercept;
18use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
19use aws_smithy_types::config_bag::ConfigBag;
20use aws_types::app_name::AppName;
21use aws_types::os_shim_internal::Env;
22
23use crate::sdk_feature::AwsSdkFeature;
24use crate::user_agent::metrics::ProvideBusinessMetric;
25use crate::user_agent::{AdditionalMetadata, ApiMetadata, AwsUserAgent, InvalidMetadataValue};
26
27#[allow(clippy::declare_interior_mutable_const)] const X_AMZ_USER_AGENT: HeaderName = HeaderName::from_static("x-amz-user-agent");
29
30#[derive(Debug)]
31enum UserAgentInterceptorError {
32 MissingApiMetadata,
33 InvalidHeaderValue(InvalidHeaderValue),
34 InvalidMetadataValue(InvalidMetadataValue),
35}
36
37impl std::error::Error for UserAgentInterceptorError {
38 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
39 match self {
40 Self::InvalidHeaderValue(source) => Some(source),
41 Self::InvalidMetadataValue(source) => Some(source),
42 Self::MissingApiMetadata => None,
43 }
44 }
45}
46
47impl fmt::Display for UserAgentInterceptorError {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 f.write_str(match self {
50 Self::InvalidHeaderValue(_) => "AwsUserAgent generated an invalid HTTP header value. This is a bug. Please file an issue.",
51 Self::InvalidMetadataValue(_) => "AwsUserAgent generated an invalid metadata value. This is a bug. Please file an issue.",
52 Self::MissingApiMetadata => "The UserAgentInterceptor requires ApiMetadata to be set before the request is made. This is a bug. Please file an issue.",
53 })
54 }
55}
56
57impl From<InvalidHeaderValue> for UserAgentInterceptorError {
58 fn from(err: InvalidHeaderValue) -> Self {
59 UserAgentInterceptorError::InvalidHeaderValue(err)
60 }
61}
62
63impl From<InvalidMetadataValue> for UserAgentInterceptorError {
64 fn from(err: InvalidMetadataValue) -> Self {
65 UserAgentInterceptorError::InvalidMetadataValue(err)
66 }
67}
68
69#[non_exhaustive]
71#[derive(Debug, Default)]
72pub struct UserAgentInterceptor;
73
74impl UserAgentInterceptor {
75 pub fn new() -> Self {
77 UserAgentInterceptor
78 }
79}
80
81fn header_values(
82 ua: &AwsUserAgent,
83) -> Result<(HeaderValue, HeaderValue), UserAgentInterceptorError> {
84 Ok((
86 HeaderValue::try_from(ua.ua_header())?,
87 HeaderValue::try_from(ua.aws_ua_header())?,
88 ))
89}
90
91impl Intercept for UserAgentInterceptor {
92 fn name(&self) -> &'static str {
93 "UserAgentInterceptor"
94 }
95
96 fn read_after_serialization(
97 &self,
98 _context: &BeforeTransmitInterceptorContextRef<'_>,
99 _runtime_components: &RuntimeComponents,
100 cfg: &mut ConfigBag,
101 ) -> Result<(), BoxError> {
102 if cfg.load::<AwsUserAgent>().is_some() {
106 return Ok(());
107 }
108
109 let api_metadata = cfg
110 .load::<ApiMetadata>()
111 .ok_or(UserAgentInterceptorError::MissingApiMetadata)?;
112 let mut ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata.clone());
113
114 let maybe_app_name = cfg.load::<AppName>();
115 if let Some(app_name) = maybe_app_name {
116 ua.set_app_name(app_name.clone());
117 }
118
119 cfg.interceptor_state().store_put(ua);
120
121 Ok(())
122 }
123
124 fn modify_before_signing(
125 &self,
126 context: &mut BeforeTransmitInterceptorContextMut<'_>,
127 runtime_components: &RuntimeComponents,
128 cfg: &mut ConfigBag,
129 ) -> Result<(), BoxError> {
130 let mut ua = cfg
131 .load::<AwsUserAgent>()
132 .expect("`AwsUserAgent should have been created in `read_before_execution`")
133 .clone();
134
135 let smithy_sdk_features = cfg.load::<SmithySdkFeature>();
136 for smithy_sdk_feature in smithy_sdk_features {
137 smithy_sdk_feature
138 .provide_business_metric()
139 .map(|m| ua.add_business_metric(m));
140 }
141
142 let aws_sdk_features = cfg.load::<AwsSdkFeature>();
143 for aws_sdk_feature in aws_sdk_features {
144 aws_sdk_feature
145 .provide_business_metric()
146 .map(|m| ua.add_business_metric(m));
147 }
148
149 let maybe_connector_metadata = runtime_components
150 .http_client()
151 .and_then(|c| c.connector_metadata());
152 if let Some(connector_metadata) = maybe_connector_metadata {
153 let am = AdditionalMetadata::new(Cow::Owned(connector_metadata.to_string()))?;
154 ua.add_additional_metadata(am);
155 }
156
157 let headers = context.request_mut().headers_mut();
158 let (user_agent, x_amz_user_agent) = header_values(&ua)?;
159 headers.append(USER_AGENT, user_agent);
160 headers.append(X_AMZ_USER_AGENT, x_amz_user_agent);
161 Ok(())
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext};
169 use aws_smithy_runtime_api::client::interceptors::Intercept;
170 use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
171 use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
172 use aws_smithy_types::config_bag::{ConfigBag, Layer};
173 use aws_smithy_types::error::display::DisplayErrorContext;
174
175 fn expect_header<'a>(context: &'a InterceptorContext, header_name: &str) -> &'a str {
176 context
177 .request()
178 .expect("request is set")
179 .headers()
180 .get(header_name)
181 .unwrap()
182 }
183
184 fn context() -> InterceptorContext {
185 let mut context = InterceptorContext::new(Input::doesnt_matter());
186 context.enter_serialization_phase();
187 context.set_request(HttpRequest::empty());
188 let _ = context.take_input();
189 context.enter_before_transmit_phase();
190 context
191 }
192
193 #[test]
194 fn test_overridden_ua() {
195 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
196 let mut context = context();
197
198 let mut layer = Layer::new("test");
199 layer.store_put(AwsUserAgent::for_tests());
200 layer.store_put(ApiMetadata::new("unused", "unused"));
201 let mut cfg = ConfigBag::of_layers(vec![layer]);
202
203 let interceptor = UserAgentInterceptor::new();
204 let mut ctx = Into::into(&mut context);
205 interceptor
206 .modify_before_signing(&mut ctx, &rc, &mut cfg)
207 .unwrap();
208
209 let header = expect_header(&context, "user-agent");
210 assert_eq!(AwsUserAgent::for_tests().ua_header(), header);
211 assert!(!header.contains("unused"));
212
213 assert_eq!(
214 AwsUserAgent::for_tests().aws_ua_header(),
215 expect_header(&context, "x-amz-user-agent")
216 );
217 }
218
219 #[test]
220 fn test_default_ua() {
221 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
222 let mut context = context();
223
224 let api_metadata = ApiMetadata::new("some-service", "some-version");
225 let mut layer = Layer::new("test");
226 layer.store_put(api_metadata.clone());
227 let mut config = ConfigBag::of_layers(vec![layer]);
228
229 let interceptor = UserAgentInterceptor::new();
230 let ctx = Into::into(&context);
231 interceptor
232 .read_after_serialization(&ctx, &rc, &mut config)
233 .unwrap();
234 let mut ctx = Into::into(&mut context);
235 interceptor
236 .modify_before_signing(&mut ctx, &rc, &mut config)
237 .unwrap();
238
239 let expected_ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata);
240 assert!(
241 expected_ua.aws_ua_header().contains("some-service"),
242 "precondition"
243 );
244 assert_eq!(
245 expected_ua.ua_header(),
246 expect_header(&context, "user-agent")
247 );
248 assert_eq!(
249 expected_ua.aws_ua_header(),
250 expect_header(&context, "x-amz-user-agent")
251 );
252 }
253
254 #[test]
255 fn test_app_name() {
256 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
257 let mut context = context();
258
259 let api_metadata = ApiMetadata::new("some-service", "some-version");
260 let mut layer = Layer::new("test");
261 layer.store_put(api_metadata);
262 layer.store_put(AppName::new("my_awesome_app").unwrap());
263 let mut config = ConfigBag::of_layers(vec![layer]);
264
265 let interceptor = UserAgentInterceptor::new();
266 let ctx = Into::into(&context);
267 interceptor
268 .read_after_serialization(&ctx, &rc, &mut config)
269 .unwrap();
270 let mut ctx = Into::into(&mut context);
271 interceptor
272 .modify_before_signing(&mut ctx, &rc, &mut config)
273 .unwrap();
274
275 let app_value = "app/my_awesome_app";
276 let header = expect_header(&context, "user-agent");
277 assert!(
278 !header.contains(app_value),
279 "expected `{header}` to not contain `{app_value}`"
280 );
281
282 let header = expect_header(&context, "x-amz-user-agent");
283 assert!(
284 header.contains(app_value),
285 "expected `{header}` to contain `{app_value}`"
286 );
287 }
288
289 #[test]
290 fn test_api_metadata_missing() {
291 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
292 let context = context();
293 let mut config = ConfigBag::base();
294
295 let interceptor = UserAgentInterceptor::new();
296 let ctx = Into::into(&context);
297
298 let error = format!(
299 "{}",
300 DisplayErrorContext(
301 &*interceptor
302 .read_after_serialization(&ctx, &rc, &mut config)
303 .expect_err("it should error")
304 )
305 );
306 assert!(
307 error.contains("This is a bug"),
308 "`{error}` should contain message `This is a bug`"
309 );
310 }
311
312 #[test]
313 fn test_api_metadata_missing_with_ua_override() {
314 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
315 let mut context = context();
316
317 let mut layer = Layer::new("test");
318 layer.store_put(AwsUserAgent::for_tests());
319 let mut config = ConfigBag::of_layers(vec![layer]);
320
321 let interceptor = UserAgentInterceptor::new();
322 let mut ctx = Into::into(&mut context);
323
324 interceptor
325 .modify_before_signing(&mut ctx, &rc, &mut config)
326 .expect("it should succeed");
327
328 let header = expect_header(&context, "user-agent");
329 assert_eq!(AwsUserAgent::for_tests().ua_header(), header);
330 assert!(!header.contains("unused"));
331
332 assert_eq!(
333 AwsUserAgent::for_tests().aws_ua_header(),
334 expect_header(&context, "x-amz-user-agent")
335 );
336 }
337}