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