aws_smithy_runtime/client/
defaults.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Runtime plugins that provide defaults for clients.
7//!
8//! Note: these are the absolute base-level defaults. They may not be the defaults
9//! for _your_ client, since many things can change these defaults on the way to
10//! code generating and constructing a full client.
11
12use 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
37/// Default connect timeout for all clients with BehaviorVersion >= v2026_01_12
38const 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/// Runtime plugin that provides a default connector.
59#[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
68/// Runtime plugin that provides a default HTTPS connector.
69pub 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        // the latest https stack takes precedence if the config flag
76        // is enabled otherwise try to fall back to the legacy connector
77        // if that feature flag is available.
78        #[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        // takes precedence over legacy connector if enabled
88        #[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        // fallback to legacy hyper client for given behavior version
96        #[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
111/// Runtime plugin that provides a default async sleep implementation.
112pub 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
121/// Runtime plugin that provides a default time source.
122pub 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
131/// Runtime plugin that sets the default retry strategy, config (disabled), and partition.
132pub 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
155/// Runtime plugin that sets the default retry strategy, config, and partition.
156///
157/// This version respects the behavior version to enable retries by default for newer versions.
158/// For AWS SDK clients with BehaviorVersion >= v2026_01_12, retries are enabled by default.
159pub 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
215/// Runtime plugin that sets the default timeout config (no timeouts).
216pub 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
230/// Runtime plugin that sets the default timeout config.
231///
232/// This version respects the behavior version to enable connection timeout by default for newer versions.
233/// For all clients with BehaviorVersion >= v2026_01_12, a 3.1s connection timeout is set.
234pub 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                // All clients with BMV >= v2026_01_12: Set connect_timeout only
249                TimeoutConfig::builder()
250                    .connect_timeout(DEFAULT_CONNECT_TIMEOUT)
251                    .build()
252            } else {
253                // Old behavior versions: All timeouts disabled
254                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
281/// Runtime plugin that registers the default identity cache implementation.
282pub 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/// Runtime plugin that sets the default stalled stream protection config.
292///
293/// By default, when throughput falls below 1/Bs for more than 5 seconds, the
294/// stream is cancelled.
295#[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            // Before v2024_03_28, upload streams did not have stalled stream protection by default
319            #[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/// Arguments for the [`default_plugins`] method.
362///
363/// This is a struct to enable adding new parameters in the future without breaking the API.
364#[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    /// Creates a new [`DefaultPluginParams`].
374    pub fn new() -> Self {
375        Default::default()
376    }
377
378    /// Sets the retry partition name.
379    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    /// Sets the behavior major version.
385    pub fn with_behavior_version(mut self, version: BehaviorVersion) -> Self {
386        self.behavior_version = Some(version);
387        self
388    }
389
390    /// Marks this as an AWS SDK client (enables retries by default for newer behavior versions).
391    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
397/// All default plugins.
398pub 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(&params),
409        default_sleep_impl_plugin(),
410        default_time_source_plugin(),
411        default_timeout_config_plugin_v2(&params),
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) // Default to non-AWS SDK for existing tests
430    }
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(&params).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        // Any version before v2026_01_12 should have retries disabled
488        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(&params).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        // v2026_01_12 is the cutoff - retries should be enabled from this version onwards
509        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(&params).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(&params).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        // This test demonstrates the complete behavior:
551        // AWS SDK clients get retries enabled ONLY when BehaviorVersion >= v2026_01_12
552
553        // Test all behavior versions
554        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(&params).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        // This test simulates the complete flow as it would happen in a real AWS SDK client
589        // It verifies that default_plugins() correctly applies retry config based on
590        // both is_aws_sdk flag and BehaviorVersion
591
592        // Scenario 1: AWS SDK with latest behavior version -> retries enabled
593        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        // Scenario 2: AWS SDK with old behavior version -> retries disabled
609        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        // Scenario 3: Non-AWS SDK (generic Smithy client) -> retries always disabled
625        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        // Scenario 4: Verify the cutoff version v2026_01_12 is the exact boundary
641        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}