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