Skip to main content

aws_config/default_provider/
retry_config.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use crate::provider_config::ProviderConfig;
7use crate::retry::error::{RetryConfigError, RetryConfigErrorKind};
8use aws_runtime::env_config::{EnvConfigError, EnvConfigValue};
9use aws_smithy_types::error::display::DisplayErrorContext;
10use aws_smithy_types::retry::{RetryConfig, RetryMode, RetrySpec};
11use std::str::FromStr;
12
13/// Default RetryConfig Provider chain
14///
15/// Unlike other "providers" `RetryConfig` has no related `RetryConfigProvider` trait. Instead,
16/// a builder struct is returned which has a similar API.
17///
18/// This provider will check the following sources in order:
19/// 1. Environment variables: `AWS_MAX_ATTEMPTS` & `AWS_RETRY_MODE`
20/// 2. Profile file: `max_attempts` and `retry_mode`
21///
22/// # Example
23///
24/// When running [`aws_config::from_env()`](crate::from_env()), a [`ConfigLoader`](crate::ConfigLoader)
25/// is created that will then create a [`RetryConfig`] from the default_provider. There is no
26/// need to call `default_provider` and the example below is only for illustration purposes.
27///
28/// ```no_run
29/// # use std::error::Error;
30/// # #[tokio::main]
31/// # async fn main() -> Result<(), Box<dyn Error>> {
32/// use aws_config::default_provider::retry_config;
33///
34/// // Load a retry config from a specific profile
35/// let retry_config = retry_config::default_provider()
36///     .profile_name("other_profile")
37///     .retry_config()
38///     .await;
39/// let config = aws_config::from_env()
40///     // Override the retry config set by the default profile
41///     .retry_config(retry_config)
42///     .load()
43///     .await;
44/// // instantiate a service client:
45/// // <my_aws_service>::Client::new(&config);
46/// #     Ok(())
47/// # }
48/// ```
49pub fn default_provider() -> Builder {
50    Builder::default()
51}
52
53mod env {
54    pub(super) const MAX_ATTEMPTS: &str = "AWS_MAX_ATTEMPTS";
55    pub(super) const RETRY_MODE: &str = "AWS_RETRY_MODE";
56    pub(super) const NEW_RETRIES_2026: &str = "AWS_NEW_RETRIES_2026";
57}
58
59mod profile_keys {
60    pub(super) const MAX_ATTEMPTS: &str = "max_attempts";
61    pub(super) const RETRY_MODE: &str = "retry_mode";
62}
63
64/// Builder for RetryConfig that checks the environment and aws profile for configuration
65#[derive(Debug, Default)]
66pub struct Builder {
67    provider_config: ProviderConfig,
68}
69
70impl Builder {
71    /// Configure the default chain
72    ///
73    /// Exposed for overriding the environment when unit-testing providers
74    pub fn configure(mut self, configuration: &ProviderConfig) -> Self {
75        self.provider_config = configuration.clone();
76        self
77    }
78
79    /// Override the profile name used by this provider
80    pub fn profile_name(mut self, name: &str) -> Self {
81        self.provider_config = self.provider_config.with_profile_name(name.to_string());
82        self
83    }
84
85    /// Attempt to create a [`RetryConfig`] from following sources in order:
86    /// 1. Environment variables: `AWS_MAX_ATTEMPTS` & `AWS_RETRY_MODE`
87    /// 2. Profile file: `max_attempts` and `retry_mode`
88    /// 3. [RetryConfig::standard()](aws_smithy_types::retry::RetryConfig::standard)
89    ///
90    /// Precedence is considered on a per-field basis
91    ///
92    /// # Panics
93    ///
94    /// - Panics if the `AWS_MAX_ATTEMPTS` env var or `max_attempts` profile var is set to 0
95    /// - Panics if the `AWS_RETRY_MODE` env var or `retry_mode` profile var is set to "adaptive" (it's not yet supported)
96    pub async fn retry_config(self) -> RetryConfig {
97        match self.try_retry_config().await {
98            Ok(conf) => conf,
99            Err(e) => panic!("{}", DisplayErrorContext(e)),
100        }
101    }
102
103    pub(crate) async fn try_retry_config(
104        self,
105    ) -> Result<RetryConfig, EnvConfigError<RetryConfigError>> {
106        let env = self.provider_config.env();
107        let profiles = self.provider_config.profile().await;
108        // Both of these can return errors due to invalid config settings, and we want to surface those as early as possible
109        // hence, we'll panic if any config values are invalid (missing values are OK though)
110        // We match this instead of unwrapping, so we can print the error with the `Display` impl instead of the `Debug` impl that unwrap uses
111        let mut retry_config = RetryConfig::standard();
112        let max_attempts = EnvConfigValue::new()
113            .env(env::MAX_ATTEMPTS)
114            .profile(profile_keys::MAX_ATTEMPTS)
115            .validate(&env, profiles, validate_max_attempts);
116
117        let retry_mode = EnvConfigValue::new()
118            .env(env::RETRY_MODE)
119            .profile(profile_keys::RETRY_MODE)
120            .validate(&env, profiles, |s| {
121                RetryMode::from_str(s)
122                    .map_err(|err| RetryConfigErrorKind::InvalidRetryMode { source: err }.into())
123            });
124
125        if let Some(max_attempts) = max_attempts? {
126            retry_config = retry_config.with_max_attempts(max_attempts);
127        }
128
129        if let Some(retry_mode) = retry_mode? {
130            retry_config = retry_config.with_retry_mode(retry_mode);
131        }
132
133        // Enable Retry Behavior 2.1 when AWS_NEW_RETRIES_2026=true
134        let new_retries =
135            EnvConfigValue::new()
136                .env(env::NEW_RETRIES_2026)
137                .validate(&env, profiles, |s| Ok::<_, RetryConfigError>(s.to_owned()));
138        if let Some(val) = new_retries? {
139            if val.eq_ignore_ascii_case("true") {
140                retry_config = retry_config.with_retry_spec(RetrySpec::v2_1());
141            }
142        }
143
144        Ok(retry_config)
145    }
146}
147
148fn validate_max_attempts(max_attempts: &str) -> Result<u32, RetryConfigError> {
149    match max_attempts.parse::<u32>() {
150        Ok(0) => Err(RetryConfigErrorKind::MaxAttemptsMustNotBeZero.into()),
151        Ok(max_attempts) => Ok(max_attempts),
152        Err(source) => Err(RetryConfigErrorKind::FailedToParseMaxAttempts { source }.into()),
153    }
154}
155
156#[cfg(test)]
157mod test {
158    use crate::default_provider::retry_config::env;
159    use crate::provider_config::ProviderConfig;
160    use crate::retry::{
161        error::RetryConfigError, error::RetryConfigErrorKind, RetryConfig, RetryMode,
162    };
163    use aws_runtime::env_config::EnvConfigError;
164    use aws_types::os_shim_internal::{Env, Fs};
165
166    async fn test_provider(
167        vars: &[(&str, &str)],
168    ) -> Result<RetryConfig, EnvConfigError<RetryConfigError>> {
169        super::Builder::default()
170            .configure(&ProviderConfig::no_configuration().with_env(Env::from_slice(vars)))
171            .try_retry_config()
172            .await
173    }
174
175    #[tokio::test]
176    async fn test_returns_default_retry_config_from_empty_profile() {
177        let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]);
178        let fs = Fs::from_slice(&[("config", "[default]\n")]);
179
180        let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
181
182        let actual_retry_config = super::default_provider()
183            .configure(&provider_config)
184            .retry_config()
185            .await;
186
187        let expected_retry_config = RetryConfig::standard();
188
189        assert_eq!(actual_retry_config, expected_retry_config);
190        // This is redundant, but it's really important to make sure that
191        // we're setting these exact values by default, so we check twice
192        assert_eq!(actual_retry_config.max_attempts(), 3);
193        assert_eq!(actual_retry_config.mode(), RetryMode::Standard);
194    }
195
196    #[tokio::test]
197    async fn test_no_retry_config_in_empty_profile() {
198        let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]);
199        let fs = Fs::from_slice(&[("config", "[default]\n")]);
200
201        let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
202
203        let actual_retry_config = super::default_provider()
204            .configure(&provider_config)
205            .retry_config()
206            .await;
207
208        let expected_retry_config = RetryConfig::standard();
209
210        assert_eq!(actual_retry_config, expected_retry_config)
211    }
212
213    #[tokio::test]
214    async fn test_creation_of_retry_config_from_profile() {
215        let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]);
216        // TODO(https://github.com/awslabs/aws-sdk-rust/issues/247): standard is the default mode;
217        // this test would be better if it was setting it to adaptive mode
218        // adaptive mode is currently unsupported so that would panic
219        let fs = Fs::from_slice(&[(
220            "config",
221            // If the lines with the vars have preceding spaces, they don't get read
222            r#"[default]
223max_attempts = 1
224retry_mode = standard
225            "#,
226        )]);
227
228        let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
229
230        let actual_retry_config = super::default_provider()
231            .configure(&provider_config)
232            .retry_config()
233            .await;
234
235        let expected_retry_config = RetryConfig::standard().with_max_attempts(1);
236
237        assert_eq!(actual_retry_config, expected_retry_config)
238    }
239
240    #[tokio::test]
241    async fn test_env_retry_config_takes_precedence_over_profile_retry_config() {
242        let env = Env::from_slice(&[
243            ("AWS_CONFIG_FILE", "config"),
244            ("AWS_MAX_ATTEMPTS", "42"),
245            ("AWS_RETRY_MODE", "standard"),
246        ]);
247        // TODO(https://github.com/awslabs/aws-sdk-rust/issues/247) standard is the default mode;
248        // this test would be better if it was setting it to adaptive mode
249        // adaptive mode is currently unsupported so that would panic
250        let fs = Fs::from_slice(&[(
251            "config",
252            // If the lines with the vars have preceding spaces, they don't get read
253            r#"[default]
254max_attempts = 88
255retry_mode = standard
256            "#,
257        )]);
258
259        let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
260
261        let actual_retry_config = super::default_provider()
262            .configure(&provider_config)
263            .retry_config()
264            .await;
265
266        let expected_retry_config = RetryConfig::standard().with_max_attempts(42);
267
268        assert_eq!(actual_retry_config, expected_retry_config)
269    }
270
271    #[tokio::test]
272    #[should_panic = "failed to parse max attempts. source: global profile (`default`) key: `max_attempts`: invalid digit found in string"]
273    async fn test_invalid_profile_retry_config_panics() {
274        let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]);
275        let fs = Fs::from_slice(&[(
276            "config",
277            // If the lines with the vars have preceding spaces, they don't get read
278            r#"[default]
279max_attempts = potato
280            "#,
281        )]);
282
283        let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
284
285        let _ = super::default_provider()
286            .configure(&provider_config)
287            .retry_config()
288            .await;
289    }
290
291    #[tokio::test]
292    async fn defaults() {
293        let built = test_provider(&[]).await.unwrap();
294
295        assert_eq!(built.mode(), RetryMode::Standard);
296        assert_eq!(built.max_attempts(), 3);
297    }
298
299    #[tokio::test]
300    async fn max_attempts_is_read_correctly() {
301        assert_eq!(
302            test_provider(&[(env::MAX_ATTEMPTS, "88")]).await.unwrap(),
303            RetryConfig::standard().with_max_attempts(88)
304        );
305    }
306
307    #[tokio::test]
308    async fn max_attempts_errors_when_it_cant_be_parsed_as_an_integer() {
309        assert!(matches!(
310            test_provider(&[(env::MAX_ATTEMPTS, "not an integer")])
311                .await
312                .unwrap_err()
313                .err(),
314            RetryConfigError {
315                kind: RetryConfigErrorKind::FailedToParseMaxAttempts { .. }
316            }
317        ));
318    }
319
320    #[tokio::test]
321    async fn retry_mode_is_read_correctly() {
322        assert_eq!(
323            test_provider(&[(env::RETRY_MODE, "standard")])
324                .await
325                .unwrap(),
326            RetryConfig::standard()
327        );
328    }
329
330    #[tokio::test]
331    async fn both_fields_can_be_set_at_once() {
332        assert_eq!(
333            test_provider(&[(env::RETRY_MODE, "standard"), (env::MAX_ATTEMPTS, "13")])
334                .await
335                .unwrap(),
336            RetryConfig::standard().with_max_attempts(13)
337        );
338    }
339
340    #[tokio::test]
341    async fn disallow_zero_max_attempts() {
342        let err = test_provider(&[(env::MAX_ATTEMPTS, "0")])
343            .await
344            .unwrap_err();
345        let err = err.err();
346        assert!(matches!(
347            err,
348            RetryConfigError {
349                kind: RetryConfigErrorKind::MaxAttemptsMustNotBeZero
350            }
351        ));
352    }
353
354    #[tokio::test]
355    async fn new_retries_env_var_enables_retry_spec_v2_1() {
356        use aws_smithy_types::retry::RetrySpec;
357        let config = test_provider(&[(env::NEW_RETRIES_2026, "true")])
358            .await
359            .unwrap();
360        assert_eq!(config.retry_spec(), Some(&RetrySpec::v2_1()));
361    }
362
363    #[tokio::test]
364    async fn new_retries_env_var_case_insensitive() {
365        use aws_smithy_types::retry::RetrySpec;
366        let config = test_provider(&[(env::NEW_RETRIES_2026, "True")])
367            .await
368            .unwrap();
369        assert_eq!(config.retry_spec(), Some(&RetrySpec::v2_1()));
370    }
371
372    #[tokio::test]
373    async fn new_retries_env_var_not_set_means_no_retry_spec() {
374        let config = test_provider(&[]).await.unwrap();
375        assert_eq!(config.retry_spec(), None);
376    }
377}