aws_smithy_runtime/client/
defaults.rs1use crate::client::http::body::content_length_enforcement::EnforceContentLengthRuntimePlugin;
13use crate::client::identity::IdentityCache;
14use crate::client::retries::strategy::standard::TokenBucketProvider;
15use crate::client::retries::strategy::StandardRetryStrategy;
16use crate::client::retries::RetryPartition;
17use aws_smithy_async::rt::sleep::default_async_sleep;
18use aws_smithy_async::time::SystemTimeSource;
19use aws_smithy_runtime_api::box_error::BoxError;
20use aws_smithy_runtime_api::client::behavior_version::BehaviorVersion;
21use aws_smithy_runtime_api::client::http::SharedHttpClient;
22use aws_smithy_runtime_api::client::runtime_components::{
23 RuntimeComponentsBuilder, SharedConfigValidator,
24};
25use aws_smithy_runtime_api::client::runtime_plugin::{
26 Order, SharedRuntimePlugin, StaticRuntimePlugin,
27};
28use aws_smithy_runtime_api::client::stalled_stream_protection::StalledStreamProtectionConfig;
29use aws_smithy_runtime_api::shared::IntoShared;
30use aws_smithy_types::config_bag::{ConfigBag, FrozenLayer, Layer};
31use aws_smithy_types::retry::RetryConfig;
32use aws_smithy_types::timeout::TimeoutConfig;
33use std::borrow::Cow;
34use std::time::Duration;
35
36fn default_plugin<CompFn>(name: &'static str, components_fn: CompFn) -> StaticRuntimePlugin
37where
38 CompFn: FnOnce(RuntimeComponentsBuilder) -> RuntimeComponentsBuilder,
39{
40 StaticRuntimePlugin::new()
41 .with_order(Order::Defaults)
42 .with_runtime_components((components_fn)(RuntimeComponentsBuilder::new(name)))
43}
44
45fn layer<LayerFn>(name: &'static str, layer_fn: LayerFn) -> FrozenLayer
46where
47 LayerFn: FnOnce(&mut Layer),
48{
49 let mut layer = Layer::new(name);
50 (layer_fn)(&mut layer);
51 layer.freeze()
52}
53
54#[deprecated(
56 since = "1.8.0",
57 note = "This function wasn't intended to be public, and didn't take the behavior major version as an argument, so it couldn't be evolved over time."
58)]
59pub fn default_http_client_plugin() -> Option<SharedRuntimePlugin> {
60 #[allow(deprecated)]
61 default_http_client_plugin_v2(BehaviorVersion::v2024_03_28())
62}
63
64pub fn default_http_client_plugin_v2(
66 behavior_version: BehaviorVersion,
67) -> Option<SharedRuntimePlugin> {
68 let mut _default: Option<SharedHttpClient> = None;
69
70 #[allow(deprecated)]
71 if behavior_version.is_at_least(BehaviorVersion::v2025_01_17()) {
72 #[cfg(all(
76 feature = "connector-hyper-0-14-x",
77 not(feature = "default-https-client")
78 ))]
79 #[allow(deprecated)]
80 {
81 _default = crate::client::http::hyper_014::default_client();
82 }
83
84 #[cfg(feature = "default-https-client")]
86 {
87 let opts = crate::client::http::DefaultClientOptions::default()
88 .with_behavior_version(behavior_version);
89 _default = crate::client::http::default_https_client(opts);
90 }
91 } else {
92 #[cfg(feature = "connector-hyper-0-14-x")]
94 #[allow(deprecated)]
95 {
96 _default = crate::client::http::hyper_014::default_client();
97 }
98 }
99
100 _default.map(|default| {
101 default_plugin("default_http_client_plugin", |components| {
102 components.with_http_client(Some(default))
103 })
104 .into_shared()
105 })
106}
107
108pub fn default_sleep_impl_plugin() -> Option<SharedRuntimePlugin> {
110 default_async_sleep().map(|default| {
111 default_plugin("default_sleep_impl_plugin", |components| {
112 components.with_sleep_impl(Some(default))
113 })
114 .into_shared()
115 })
116}
117
118pub fn default_time_source_plugin() -> Option<SharedRuntimePlugin> {
120 Some(
121 default_plugin("default_time_source_plugin", |components| {
122 components.with_time_source(Some(SystemTimeSource::new()))
123 })
124 .into_shared(),
125 )
126}
127
128pub fn default_retry_config_plugin(
130 default_partition_name: impl Into<Cow<'static, str>>,
131) -> Option<SharedRuntimePlugin> {
132 let retry_partition = RetryPartition::new(default_partition_name);
133 Some(
134 default_plugin("default_retry_config_plugin", |components| {
135 components
136 .with_retry_strategy(Some(StandardRetryStrategy::new()))
137 .with_config_validator(SharedConfigValidator::base_client_config_fn(
138 validate_retry_config,
139 ))
140 .with_interceptor(TokenBucketProvider::new(retry_partition.clone()))
141 })
142 .with_config(layer("default_retry_config", |layer| {
143 layer.store_put(RetryConfig::disabled());
144 layer.store_put(retry_partition);
145 }))
146 .into_shared(),
147 )
148}
149
150pub fn default_retry_config_plugin_v2(params: &DefaultPluginParams) -> Option<SharedRuntimePlugin> {
155 let default_partition_name = params.retry_partition_name.as_ref()?.clone();
156 let is_aws_sdk = params.is_aws_sdk;
157 let behavior_version = params
158 .behavior_version
159 .unwrap_or_else(BehaviorVersion::latest);
160 let retry_partition = RetryPartition::new(default_partition_name);
161 Some(
162 default_plugin("default_retry_config_plugin", |components| {
163 components
164 .with_retry_strategy(Some(StandardRetryStrategy::new()))
165 .with_config_validator(SharedConfigValidator::base_client_config_fn(
166 validate_retry_config,
167 ))
168 .with_interceptor(TokenBucketProvider::new(retry_partition.clone()))
169 })
170 .with_config(layer("default_retry_config", |layer| {
171 #[allow(deprecated)]
172 let retry_config =
173 if is_aws_sdk && behavior_version.is_at_least(BehaviorVersion::v2025_01_17()) {
174 RetryConfig::standard()
175 } else {
176 RetryConfig::disabled()
177 };
178 layer.store_put(retry_config);
179 layer.store_put(retry_partition);
180 }))
181 .into_shared(),
182 )
183}
184
185fn validate_retry_config(
186 components: &RuntimeComponentsBuilder,
187 cfg: &ConfigBag,
188) -> Result<(), BoxError> {
189 if let Some(retry_config) = cfg.load::<RetryConfig>() {
190 if retry_config.has_retry() && components.sleep_impl().is_none() {
191 Err("An async sleep implementation is required for retry to work. Please provide a `sleep_impl` on \
192 the config, or disable timeouts.".into())
193 } else {
194 Ok(())
195 }
196 } else {
197 Err(
198 "The default retry config was removed, and no other config was put in its place."
199 .into(),
200 )
201 }
202}
203
204pub fn default_timeout_config_plugin() -> Option<SharedRuntimePlugin> {
206 Some(
207 default_plugin("default_timeout_config_plugin", |components| {
208 components.with_config_validator(SharedConfigValidator::base_client_config_fn(
209 validate_timeout_config,
210 ))
211 })
212 .with_config(layer("default_timeout_config", |layer| {
213 layer.store_put(TimeoutConfig::disabled());
214 }))
215 .into_shared(),
216 )
217}
218
219pub fn default_timeout_config_plugin_v2(
224 params: &DefaultPluginParams,
225) -> Option<SharedRuntimePlugin> {
226 let behavior_version = params
227 .behavior_version
228 .unwrap_or_else(BehaviorVersion::latest);
229 let is_aws_sdk = params.is_aws_sdk;
230 Some(
231 default_plugin("default_timeout_config_plugin", |components| {
232 components.with_config_validator(SharedConfigValidator::base_client_config_fn(
233 validate_timeout_config,
234 ))
235 })
236 .with_config(layer("default_timeout_config", |layer| {
237 #[allow(deprecated)]
238 let timeout_config =
239 if is_aws_sdk && behavior_version.is_at_least(BehaviorVersion::v2025_01_17()) {
240 TimeoutConfig::builder()
242 .connect_timeout(Duration::from_millis(3100))
243 .disable_operation_timeout()
244 .disable_operation_attempt_timeout()
245 .build()
246 } else {
247 TimeoutConfig::disabled()
249 };
250 layer.store_put(timeout_config);
251 }))
252 .into_shared(),
253 )
254}
255
256fn validate_timeout_config(
257 components: &RuntimeComponentsBuilder,
258 cfg: &ConfigBag,
259) -> Result<(), BoxError> {
260 if let Some(timeout_config) = cfg.load::<TimeoutConfig>() {
261 if timeout_config.has_timeouts() && components.sleep_impl().is_none() {
262 Err("An async sleep implementation is required for timeouts to work. Please provide a `sleep_impl` on \
263 the config, or disable timeouts.".into())
264 } else {
265 Ok(())
266 }
267 } else {
268 Err(
269 "The default timeout config was removed, and no other config was put in its place."
270 .into(),
271 )
272 }
273}
274
275pub fn default_identity_cache_plugin() -> Option<SharedRuntimePlugin> {
277 Some(
278 default_plugin("default_identity_cache_plugin", |components| {
279 components.with_identity_cache(Some(IdentityCache::lazy().build()))
280 })
281 .into_shared(),
282 )
283}
284
285#[deprecated(
290 since = "1.2.0",
291 note = "This function wasn't intended to be public, and didn't take the behavior major version as an argument, so it couldn't be evolved over time."
292)]
293pub fn default_stalled_stream_protection_config_plugin() -> Option<SharedRuntimePlugin> {
294 #[allow(deprecated)]
295 default_stalled_stream_protection_config_plugin_v2(BehaviorVersion::v2023_11_09())
296}
297fn default_stalled_stream_protection_config_plugin_v2(
298 behavior_version: BehaviorVersion,
299) -> Option<SharedRuntimePlugin> {
300 Some(
301 default_plugin(
302 "default_stalled_stream_protection_config_plugin",
303 |components| {
304 components.with_config_validator(SharedConfigValidator::base_client_config_fn(
305 validate_stalled_stream_protection_config,
306 ))
307 },
308 )
309 .with_config(layer("default_stalled_stream_protection_config", |layer| {
310 let mut config =
311 StalledStreamProtectionConfig::enabled().grace_period(Duration::from_secs(5));
312 #[allow(deprecated)]
314 if !behavior_version.is_at_least(BehaviorVersion::v2024_03_28()) {
315 config = config.upload_enabled(false);
316 }
317 layer.store_put(config.build());
318 }))
319 .into_shared(),
320 )
321}
322
323fn enforce_content_length_runtime_plugin() -> Option<SharedRuntimePlugin> {
324 Some(EnforceContentLengthRuntimePlugin::new().into_shared())
325}
326
327fn validate_stalled_stream_protection_config(
328 components: &RuntimeComponentsBuilder,
329 cfg: &ConfigBag,
330) -> Result<(), BoxError> {
331 if let Some(stalled_stream_protection_config) = cfg.load::<StalledStreamProtectionConfig>() {
332 if stalled_stream_protection_config.is_enabled() {
333 if components.sleep_impl().is_none() {
334 return Err(
335 "An async sleep implementation is required for stalled stream protection to work. \
336 Please provide a `sleep_impl` on the config, or disable stalled stream protection.".into());
337 }
338
339 if components.time_source().is_none() {
340 return Err(
341 "A time source is required for stalled stream protection to work.\
342 Please provide a `time_source` on the config, or disable stalled stream protection.".into());
343 }
344 }
345
346 Ok(())
347 } else {
348 Err(
349 "The default stalled stream protection config was removed, and no other config was put in its place."
350 .into(),
351 )
352 }
353}
354
355#[non_exhaustive]
359#[derive(Debug, Default)]
360pub struct DefaultPluginParams {
361 retry_partition_name: Option<Cow<'static, str>>,
362 behavior_version: Option<BehaviorVersion>,
363 is_aws_sdk: bool,
364}
365
366impl DefaultPluginParams {
367 pub fn new() -> Self {
369 Default::default()
370 }
371
372 pub fn with_retry_partition_name(mut self, name: impl Into<Cow<'static, str>>) -> Self {
374 self.retry_partition_name = Some(name.into());
375 self
376 }
377
378 pub fn with_behavior_version(mut self, version: BehaviorVersion) -> Self {
380 self.behavior_version = Some(version);
381 self
382 }
383
384 pub fn with_is_aws_sdk(mut self, is_aws_sdk: bool) -> Self {
386 self.is_aws_sdk = is_aws_sdk;
387 self
388 }
389}
390
391pub fn default_plugins(
393 params: DefaultPluginParams,
394) -> impl IntoIterator<Item = SharedRuntimePlugin> {
395 let behavior_version = params
396 .behavior_version
397 .unwrap_or_else(BehaviorVersion::latest);
398
399 [
400 default_http_client_plugin_v2(behavior_version),
401 default_identity_cache_plugin(),
402 default_retry_config_plugin_v2(¶ms),
403 default_sleep_impl_plugin(),
404 default_time_source_plugin(),
405 default_timeout_config_plugin_v2(¶ms),
406 enforce_content_length_runtime_plugin(),
407 default_stalled_stream_protection_config_plugin_v2(behavior_version),
408 ]
409 .into_iter()
410 .flatten()
411 .collect::<Vec<SharedRuntimePlugin>>()
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use aws_smithy_runtime_api::client::runtime_plugin::{RuntimePlugin, RuntimePlugins};
418
419 fn test_plugin_params(version: BehaviorVersion) -> DefaultPluginParams {
420 DefaultPluginParams::new()
421 .with_behavior_version(version)
422 .with_retry_partition_name("dontcare")
423 .with_is_aws_sdk(false) }
425 fn config_for(plugins: impl IntoIterator<Item = SharedRuntimePlugin>) -> ConfigBag {
426 let mut config = ConfigBag::base();
427 let plugins = RuntimePlugins::new().with_client_plugins(plugins);
428 plugins.apply_client_configuration(&mut config).unwrap();
429 config
430 }
431
432 #[test]
433 #[allow(deprecated)]
434 fn v2024_03_28_stalled_stream_protection_difference() {
435 let latest = config_for(default_plugins(test_plugin_params(
436 BehaviorVersion::latest(),
437 )));
438 let v2023 = config_for(default_plugins(test_plugin_params(
439 BehaviorVersion::v2023_11_09(),
440 )));
441
442 assert!(
443 latest
444 .load::<StalledStreamProtectionConfig>()
445 .unwrap()
446 .upload_enabled(),
447 "stalled stream protection on uploads MUST be enabled after v2024_03_28"
448 );
449 assert!(
450 !v2023
451 .load::<StalledStreamProtectionConfig>()
452 .unwrap()
453 .upload_enabled(),
454 "stalled stream protection on uploads MUST NOT be enabled before v2024_03_28"
455 );
456 }
457
458 #[test]
459 fn test_retry_enabled_for_aws_sdk() {
460 let params = DefaultPluginParams::new()
461 .with_retry_partition_name("test-partition")
462 .with_behavior_version(BehaviorVersion::latest())
463 .with_is_aws_sdk(true);
464 let plugin = default_retry_config_plugin_v2(¶ms).expect("plugin should be created");
465
466 let config = plugin.config().expect("config should exist");
467 let retry_config = config
468 .load::<RetryConfig>()
469 .expect("retry config should exist");
470
471 assert_eq!(
472 retry_config.max_attempts(),
473 3,
474 "retries should be enabled with max_attempts=3 for AWS SDK with latest behavior version"
475 );
476 }
477
478 #[test]
479 #[allow(deprecated)]
480 fn test_retry_disabled_for_aws_sdk_old_behavior_version() {
481 let params = DefaultPluginParams::new()
483 .with_retry_partition_name("test-partition")
484 .with_behavior_version(BehaviorVersion::v2024_03_28())
485 .with_is_aws_sdk(true);
486 let plugin = default_retry_config_plugin_v2(¶ms).expect("plugin should be created");
487
488 let config = plugin.config().expect("config should exist");
489 let retry_config = config
490 .load::<RetryConfig>()
491 .expect("retry config should exist");
492
493 assert_eq!(
494 retry_config.max_attempts(),
495 1,
496 "retries should be disabled for AWS SDK with behavior version < v2025_01_17"
497 );
498 }
499
500 #[test]
501 #[allow(deprecated)]
502 fn test_retry_enabled_at_cutoff_version() {
503 let params = DefaultPluginParams::new()
505 .with_retry_partition_name("test-partition")
506 .with_behavior_version(BehaviorVersion::v2025_01_17())
507 .with_is_aws_sdk(true);
508 let plugin = default_retry_config_plugin_v2(¶ms).expect("plugin should be created");
509
510 let config = plugin.config().expect("config should exist");
511 let retry_config = config
512 .load::<RetryConfig>()
513 .expect("retry config should exist");
514
515 assert_eq!(
516 retry_config.max_attempts(),
517 3,
518 "retries should be enabled for AWS SDK starting from v2025_01_17"
519 );
520 }
521
522 #[test]
523 fn test_retry_disabled_for_non_aws_sdk() {
524 let params = DefaultPluginParams::new()
525 .with_retry_partition_name("test-partition")
526 .with_behavior_version(BehaviorVersion::latest())
527 .with_is_aws_sdk(false);
528 let plugin = default_retry_config_plugin_v2(¶ms).expect("plugin should be created");
529
530 let config = plugin.config().expect("config should exist");
531 let retry_config = config
532 .load::<RetryConfig>()
533 .expect("retry config should exist");
534
535 assert_eq!(
536 retry_config.max_attempts(),
537 1,
538 "retries should be disabled for non-AWS SDK clients"
539 );
540 }
541
542 #[test]
543 #[allow(deprecated)]
544 fn test_behavior_version_gates_retry_for_aws_sdk() {
545 let test_cases = vec![
550 (BehaviorVersion::v2023_11_09(), 1, "v2023_11_09 (old)"),
551 (BehaviorVersion::v2024_03_28(), 1, "v2024_03_28 (old)"),
552 (BehaviorVersion::v2025_01_17(), 3, "v2025_01_17 (cutoff)"),
553 (BehaviorVersion::latest(), 3, "latest"),
554 ];
555
556 for (version, expected_attempts, version_name) in test_cases {
557 let params = DefaultPluginParams::new()
558 .with_retry_partition_name("test-partition")
559 .with_behavior_version(version)
560 .with_is_aws_sdk(true);
561
562 let plugin = default_retry_config_plugin_v2(¶ms).expect("plugin should be created");
563 let config = plugin.config().expect("config should exist");
564 let retry_config = config
565 .load::<RetryConfig>()
566 .expect("retry config should exist");
567
568 assert_eq!(
569 retry_config.max_attempts(),
570 expected_attempts,
571 "AWS SDK with {} should have {} max attempts",
572 version_name,
573 expected_attempts
574 );
575 }
576 }
577
578 #[test]
579 #[allow(deprecated)]
580 fn test_complete_default_plugins_integration() {
581 let params_aws_latest = DefaultPluginParams::new()
587 .with_retry_partition_name("aws-s3")
588 .with_behavior_version(BehaviorVersion::latest())
589 .with_is_aws_sdk(true);
590
591 let config_aws_latest = config_for(default_plugins(params_aws_latest));
592 let retry_aws_latest = config_aws_latest
593 .load::<RetryConfig>()
594 .expect("retry config should exist");
595 assert_eq!(
596 retry_aws_latest.max_attempts(),
597 3,
598 "AWS SDK with latest behavior version should have retries enabled (3 attempts)"
599 );
600
601 let params_aws_old = DefaultPluginParams::new()
603 .with_retry_partition_name("aws-s3")
604 .with_behavior_version(BehaviorVersion::v2024_03_28())
605 .with_is_aws_sdk(true);
606
607 let config_aws_old = config_for(default_plugins(params_aws_old));
608 let retry_aws_old = config_aws_old
609 .load::<RetryConfig>()
610 .expect("retry config should exist");
611 assert_eq!(
612 retry_aws_old.max_attempts(),
613 1,
614 "AWS SDK with old behavior version should have retries disabled (1 attempt)"
615 );
616
617 let params_generic = DefaultPluginParams::new()
619 .with_retry_partition_name("my-service")
620 .with_behavior_version(BehaviorVersion::latest())
621 .with_is_aws_sdk(false);
622
623 let config_generic = config_for(default_plugins(params_generic));
624 let retry_generic = config_generic
625 .load::<RetryConfig>()
626 .expect("retry config should exist");
627 assert_eq!(
628 retry_generic.max_attempts(),
629 1,
630 "Non-AWS SDK clients should always have retries disabled (1 attempt)"
631 );
632
633 let params_cutoff = DefaultPluginParams::new()
635 .with_retry_partition_name("aws-s3")
636 .with_behavior_version(BehaviorVersion::v2025_01_17())
637 .with_is_aws_sdk(true);
638
639 let config_cutoff = config_for(default_plugins(params_cutoff));
640 let retry_cutoff = config_cutoff
641 .load::<RetryConfig>()
642 .expect("retry config should exist");
643 assert_eq!(
644 retry_cutoff.max_attempts(),
645 3,
646 "AWS SDK with v2025_01_17 (the cutoff version) should have retries enabled (3 attempts)"
647 );
648 }
649}