aws_config/default_provider/
retry_config.rs1use 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
13pub 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#[derive(Debug, Default)]
66pub struct Builder {
67 provider_config: ProviderConfig,
68}
69
70impl Builder {
71 pub fn configure(mut self, configuration: &ProviderConfig) -> Self {
75 self.provider_config = configuration.clone();
76 self
77 }
78
79 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 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 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 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 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 let fs = Fs::from_slice(&[(
220 "config",
221 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 let fs = Fs::from_slice(&[(
251 "config",
252 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 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}