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::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
36/// Default connect timeout for all clients with BehaviorVersion >= v2026_01_12
37const 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/// Runtime plugin that provides a default connector.
58#[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
67/// Runtime plugin that provides a default HTTPS connector.
68pub 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        // the latest https stack takes precedence if the config flag
75        // is enabled otherwise try to fall back to the legacy connector
76        // if that feature flag is available.
77        #[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        // takes precedence over legacy connector if enabled
87        #[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        // fallback to legacy hyper client for given behavior version
95        #[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
110/// Runtime plugin that provides a default async sleep implementation.
111pub 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
120/// Runtime plugin that provides a default time source.
121pub 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
130/// Runtime plugin that sets the default retry strategy, config (disabled), and partition.
131pub 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
152/// Runtime plugin that sets the default retry strategy, config, and partition.
153///
154/// This version respects the behavior version to enable retries by default for newer versions.
155/// For AWS SDK clients with BehaviorVersion >= v2026_01_12, retries are enabled by default.
156pub 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
210/// Runtime plugin that sets the default timeout config (no timeouts).
211pub 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
225/// Runtime plugin that sets the default timeout config.
226///
227/// This version respects the behavior version to enable connection timeout by default for newer versions.
228/// For all clients with BehaviorVersion >= v2026_01_12, a 3.1s connection timeout is set.
229pub 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                // All clients with BMV >= v2026_01_12: Set connect_timeout only
244                TimeoutConfig::builder()
245                    .connect_timeout(DEFAULT_CONNECT_TIMEOUT)
246                    .build()
247            } else {
248                // Old behavior versions: All timeouts disabled
249                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
276/// Runtime plugin that registers the default identity cache implementation.
277pub 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/// Runtime plugin that sets the default stalled stream protection config.
287///
288/// By default, when throughput falls below 1/Bs for more than 5 seconds, the
289/// stream is cancelled.
290#[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            // Before v2024_03_28, upload streams did not have stalled stream protection by default
314            #[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/// Arguments for the [`default_plugins`] method.
357///
358/// This is a struct to enable adding new parameters in the future without breaking the API.
359#[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    /// Creates a new [`DefaultPluginParams`].
369    pub fn new() -> Self {
370        Default::default()
371    }
372
373    /// Sets the retry partition name.
374    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    /// Sets the behavior major version.
380    pub fn with_behavior_version(mut self, version: BehaviorVersion) -> Self {
381        self.behavior_version = Some(version);
382        self
383    }
384
385    /// Marks this as an AWS SDK client (enables retries by default for newer behavior versions).
386    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
392/// All default plugins.
393pub 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(&params),
404        default_sleep_impl_plugin(),
405        default_time_source_plugin(),
406        default_timeout_config_plugin_v2(&params),
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) // Default to non-AWS SDK for existing tests
425    }
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(&params).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        // Any version before v2026_01_12 should have retries disabled
483        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(&params).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        // v2026_01_12 is the cutoff - retries should be enabled from this version onwards
504        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(&params).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(&params).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        // This test demonstrates the complete behavior:
546        // AWS SDK clients get retries enabled ONLY when BehaviorVersion >= v2026_01_12
547
548        // Test all behavior versions
549        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(&params).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        // This test simulates the complete flow as it would happen in a real AWS SDK client
584        // It verifies that default_plugins() correctly applies retry config based on
585        // both is_aws_sdk flag and BehaviorVersion
586
587        // Scenario 1: AWS SDK with latest behavior version -> retries enabled
588        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        // Scenario 2: AWS SDK with old behavior version -> retries disabled
604        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        // Scenario 3: Non-AWS SDK (generic Smithy client) -> retries always disabled
620        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        // Scenario 4: Verify the cutoff version v2026_01_12 is the exact boundary
636        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}