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