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