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