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    #[allow(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    #[allow(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, retries are enabled by default.
154pub fn default_retry_config_plugin_v2(
155    params: &DefaultPluginParams,
156) -> Option<SharedRuntimePlugin> {
157    let default_partition_name = params.retry_partition_name.as_ref()?.clone();
158    let is_aws_sdk = params.is_aws_sdk;
159    let retry_partition = RetryPartition::new(default_partition_name);
160    Some(
161        default_plugin("default_retry_config_plugin", |components| {
162            components
163                .with_retry_strategy(Some(StandardRetryStrategy::new()))
164                .with_config_validator(SharedConfigValidator::base_client_config_fn(
165                    validate_retry_config,
166                ))
167                .with_interceptor(TokenBucketProvider::new(retry_partition.clone()))
168        })
169        .with_config(layer("default_retry_config", |layer| {
170            let retry_config = if is_aws_sdk {
171                RetryConfig::standard()
172            } else {
173                RetryConfig::disabled()
174            };
175            layer.store_put(retry_config);
176            layer.store_put(retry_partition);
177        }))
178        .into_shared(),
179    )
180}
181
182fn validate_retry_config(
183    components: &RuntimeComponentsBuilder,
184    cfg: &ConfigBag,
185) -> Result<(), BoxError> {
186    if let Some(retry_config) = cfg.load::<RetryConfig>() {
187        if retry_config.has_retry() && components.sleep_impl().is_none() {
188            Err("An async sleep implementation is required for retry to work. Please provide a `sleep_impl` on \
189                 the config, or disable timeouts.".into())
190        } else {
191            Ok(())
192        }
193    } else {
194        Err(
195            "The default retry config was removed, and no other config was put in its place."
196                .into(),
197        )
198    }
199}
200
201/// Runtime plugin that sets the default timeout config (no timeouts).
202pub fn default_timeout_config_plugin() -> Option<SharedRuntimePlugin> {
203    Some(
204        default_plugin("default_timeout_config_plugin", |components| {
205            components.with_config_validator(SharedConfigValidator::base_client_config_fn(
206                validate_timeout_config,
207            ))
208        })
209        .with_config(layer("default_timeout_config", |layer| {
210            layer.store_put(TimeoutConfig::disabled());
211        }))
212        .into_shared(),
213    )
214}
215
216/// Runtime plugin that sets the default timeout config.
217/// 
218/// This version respects the behavior version to enable connection timeout by default for newer versions.
219/// For AWS SDK clients, connection timeout is enabled by default.
220pub fn default_timeout_config_plugin_v2(
221    params: &DefaultPluginParams,
222) -> Option<SharedRuntimePlugin> {
223    let behavior_version = params.behavior_version.unwrap_or_else(BehaviorVersion::latest);
224    let is_aws_sdk = params.is_aws_sdk;
225    Some(
226        default_plugin("default_timeout_config_plugin", |components| {
227            components.with_config_validator(SharedConfigValidator::base_client_config_fn(
228                validate_timeout_config,
229            ))
230        })
231        .with_config(layer("default_timeout_config", |layer| {
232            #[allow(deprecated)]
233            let timeout_config = if is_aws_sdk && behavior_version.is_at_least(BehaviorVersion::v2025_08_07()) {
234                // AWS SDK with new behavior version: Set connect_timeout, leave operation timeouts unset
235                TimeoutConfig::builder()
236                    .connect_timeout(Duration::from_millis(3100))
237                    .build()
238            } else {
239                // Old behavior versions or non-AWS SDK: All timeouts disabled
240                TimeoutConfig::disabled()
241            };
242            layer.store_put(timeout_config);
243        }))
244        .into_shared(),
245    )
246}
247
248fn validate_timeout_config(
249    components: &RuntimeComponentsBuilder,
250    cfg: &ConfigBag,
251) -> Result<(), BoxError> {
252    if let Some(timeout_config) = cfg.load::<TimeoutConfig>() {
253        if timeout_config.has_timeouts() && components.sleep_impl().is_none() {
254            Err("An async sleep implementation is required for timeouts to work. Please provide a `sleep_impl` on \
255                 the config, or disable timeouts.".into())
256        } else {
257            Ok(())
258        }
259    } else {
260        Err(
261            "The default timeout config was removed, and no other config was put in its place."
262                .into(),
263        )
264    }
265}
266
267/// Runtime plugin that registers the default identity cache implementation.
268pub fn default_identity_cache_plugin() -> Option<SharedRuntimePlugin> {
269    Some(
270        default_plugin("default_identity_cache_plugin", |components| {
271            components.with_identity_cache(Some(IdentityCache::lazy().build()))
272        })
273        .into_shared(),
274    )
275}
276
277/// Runtime plugin that sets the default stalled stream protection config.
278///
279/// By default, when throughput falls below 1/Bs for more than 5 seconds, the
280/// stream is cancelled.
281#[deprecated(
282    since = "1.2.0",
283    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."
284)]
285pub fn default_stalled_stream_protection_config_plugin() -> Option<SharedRuntimePlugin> {
286    #[allow(deprecated)]
287    default_stalled_stream_protection_config_plugin_v2(BehaviorVersion::v2023_11_09())
288}
289fn default_stalled_stream_protection_config_plugin_v2(
290    behavior_version: BehaviorVersion,
291) -> Option<SharedRuntimePlugin> {
292    Some(
293        default_plugin(
294            "default_stalled_stream_protection_config_plugin",
295            |components| {
296                components.with_config_validator(SharedConfigValidator::base_client_config_fn(
297                    validate_stalled_stream_protection_config,
298                ))
299            },
300        )
301        .with_config(layer("default_stalled_stream_protection_config", |layer| {
302            let mut config =
303                StalledStreamProtectionConfig::enabled().grace_period(Duration::from_secs(5));
304            // Before v2024_03_28, upload streams did not have stalled stream protection by default
305            #[allow(deprecated)]
306            if !behavior_version.is_at_least(BehaviorVersion::v2024_03_28()) {
307                config = config.upload_enabled(false);
308            }
309            layer.store_put(config.build());
310        }))
311        .into_shared(),
312    )
313}
314
315fn enforce_content_length_runtime_plugin() -> Option<SharedRuntimePlugin> {
316    Some(EnforceContentLengthRuntimePlugin::new().into_shared())
317}
318
319fn validate_stalled_stream_protection_config(
320    components: &RuntimeComponentsBuilder,
321    cfg: &ConfigBag,
322) -> Result<(), BoxError> {
323    if let Some(stalled_stream_protection_config) = cfg.load::<StalledStreamProtectionConfig>() {
324        if stalled_stream_protection_config.is_enabled() {
325            if components.sleep_impl().is_none() {
326                return Err(
327                    "An async sleep implementation is required for stalled stream protection to work. \
328                     Please provide a `sleep_impl` on the config, or disable stalled stream protection.".into());
329            }
330
331            if components.time_source().is_none() {
332                return Err(
333                    "A time source is required for stalled stream protection to work.\
334                     Please provide a `time_source` on the config, or disable stalled stream protection.".into());
335            }
336        }
337
338        Ok(())
339    } else {
340        Err(
341            "The default stalled stream protection config was removed, and no other config was put in its place."
342                .into(),
343        )
344    }
345}
346
347/// Arguments for the [`default_plugins`] method.
348///
349/// This is a struct to enable adding new parameters in the future without breaking the API.
350#[non_exhaustive]
351#[derive(Debug, Default)]
352pub struct DefaultPluginParams {
353    retry_partition_name: Option<Cow<'static, str>>,
354    behavior_version: Option<BehaviorVersion>,
355    is_aws_sdk: bool,
356}
357
358impl DefaultPluginParams {
359    /// Creates a new [`DefaultPluginParams`].
360    pub fn new() -> Self {
361        Default::default()
362    }
363
364    /// Sets the retry partition name.
365    pub fn with_retry_partition_name(mut self, name: impl Into<Cow<'static, str>>) -> Self {
366        self.retry_partition_name = Some(name.into());
367        self
368    }
369
370    /// Sets the behavior major version.
371    pub fn with_behavior_version(mut self, version: BehaviorVersion) -> Self {
372        self.behavior_version = Some(version);
373        self
374    }
375
376    /// Marks this as an AWS SDK client (enables retries by default for newer behavior versions).
377    pub fn with_is_aws_sdk(mut self, is_aws_sdk: bool) -> Self {
378        self.is_aws_sdk = is_aws_sdk;
379        self
380    }
381}
382
383/// All default plugins.
384pub fn default_plugins(
385    params: DefaultPluginParams,
386) -> impl IntoIterator<Item = SharedRuntimePlugin> {
387    let behavior_version = params
388        .behavior_version
389        .unwrap_or_else(BehaviorVersion::latest);
390
391    [
392        default_http_client_plugin_v2(behavior_version),
393        default_identity_cache_plugin(),
394        default_retry_config_plugin_v2(&params),
395        default_sleep_impl_plugin(),
396        default_time_source_plugin(),
397        default_timeout_config_plugin_v2(&params),
398        enforce_content_length_runtime_plugin(),
399        default_stalled_stream_protection_config_plugin_v2(behavior_version),
400    ]
401    .into_iter()
402    .flatten()
403    .collect::<Vec<SharedRuntimePlugin>>()
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409    use aws_smithy_runtime_api::client::runtime_plugin::{RuntimePlugin, RuntimePlugins};
410
411    fn test_plugin_params(version: BehaviorVersion) -> DefaultPluginParams {
412        DefaultPluginParams::new()
413            .with_behavior_version(version)
414            .with_retry_partition_name("dontcare")
415            .with_is_aws_sdk(false) // Default to non-AWS SDK for existing tests
416    }
417    fn config_for(plugins: impl IntoIterator<Item = SharedRuntimePlugin>) -> ConfigBag {
418        let mut config = ConfigBag::base();
419        let plugins = RuntimePlugins::new().with_client_plugins(plugins);
420        plugins.apply_client_configuration(&mut config).unwrap();
421        config
422    }
423
424    #[test]
425    #[allow(deprecated)]
426    fn v2024_03_28_stalled_stream_protection_difference() {
427        let latest = config_for(default_plugins(test_plugin_params(
428            BehaviorVersion::latest(),
429        )));
430        let v2023 = config_for(default_plugins(test_plugin_params(
431            BehaviorVersion::v2023_11_09(),
432        )));
433
434        assert!(
435            latest
436                .load::<StalledStreamProtectionConfig>()
437                .unwrap()
438                .upload_enabled(),
439            "stalled stream protection on uploads MUST be enabled after v2024_03_28"
440        );
441        assert!(
442            !v2023
443                .load::<StalledStreamProtectionConfig>()
444                .unwrap()
445                .upload_enabled(),
446            "stalled stream protection on uploads MUST NOT be enabled before v2024_03_28"
447        );
448    }
449
450    #[test]
451    fn test_retry_enabled_for_aws_sdk() {
452        let params = DefaultPluginParams::new()
453            .with_retry_partition_name("test-partition")
454            .with_is_aws_sdk(true);
455        let plugin = default_retry_config_plugin_v2(&params)
456            .expect("plugin should be created");
457
458        let config = plugin.config().expect("config should exist");
459        let retry_config = config
460            .load::<RetryConfig>()
461            .expect("retry config should exist");
462
463        assert_eq!(
464            retry_config.max_attempts(),
465            3,
466            "retries should be enabled with max_attempts=3 for AWS SDK"
467        );
468    }
469
470    #[test]
471    fn test_retry_disabled_for_non_aws_sdk() {
472        let params = DefaultPluginParams::new()
473            .with_retry_partition_name("test-partition")
474            .with_is_aws_sdk(false);
475        let plugin = default_retry_config_plugin_v2(&params)
476            .expect("plugin should be created");
477
478        let config = plugin.config().expect("config should exist");
479        let retry_config = config
480            .load::<RetryConfig>()
481            .expect("retry config should exist");
482
483        assert_eq!(
484            retry_config.max_attempts(),
485            1,
486            "retries should be disabled for non-AWS SDK clients"
487        );
488    }
489}