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