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