1use aws_smithy_runtime_api::client::interceptors::context::InterceptorContext;
7use aws_smithy_runtime_api::client::orchestrator::OrchestratorError;
8use aws_smithy_runtime_api::client::retries::classifiers::{
9 ClassifyRetry, RetryAction, RetryClassifierPriority, RetryReason,
10};
11use aws_smithy_types::error::metadata::ProvideErrorMetadata;
12use aws_smithy_types::retry::ErrorKind;
13use std::borrow::Cow;
14use std::error::Error as StdError;
15use std::marker::PhantomData;
16
17fn retry_after_from_ctx(ctx: &InterceptorContext) -> Option<std::time::Duration> {
22 ctx.response()
23 .and_then(|res| res.headers().get("x-amz-retry-after"))
24 .and_then(|header| header.parse::<u64>().ok())
25 .map(std::time::Duration::from_millis)
26}
27
28pub const THROTTLING_ERRORS: &[&str] = &[
30 "Throttling",
31 "ThrottlingException",
32 "ThrottledException",
33 "RequestThrottledException",
34 "TooManyRequestsException",
35 "ProvisionedThroughputExceededException",
36 "TransactionInProgressException",
37 "RequestLimitExceeded",
38 "BandwidthLimitExceeded",
39 "LimitExceededException",
40 "RequestThrottled",
41 "SlowDown",
42 "PriorRequestNotComplete",
43 "EC2ThrottledException",
44];
45
46pub const TRANSIENT_ERRORS: &[&str] = &["RequestTimeout", "RequestTimeoutException"];
48
49#[derive(Debug)]
51pub struct AwsErrorCodeClassifier<E> {
52 throttling_errors: Cow<'static, [&'static str]>,
53 transient_errors: Cow<'static, [&'static str]>,
54 _inner: PhantomData<E>,
55}
56
57impl<E> Default for AwsErrorCodeClassifier<E> {
58 fn default() -> Self {
59 Self {
60 throttling_errors: THROTTLING_ERRORS.into(),
61 transient_errors: TRANSIENT_ERRORS.into(),
62 _inner: PhantomData,
63 }
64 }
65}
66
67#[derive(Debug)]
69pub struct AwsErrorCodeClassifierBuilder<E> {
70 throttling_errors: Option<Cow<'static, [&'static str]>>,
71 transient_errors: Option<Cow<'static, [&'static str]>>,
72 _inner: PhantomData<E>,
73}
74
75impl<E> AwsErrorCodeClassifierBuilder<E> {
76 pub fn transient_errors(
78 mut self,
79 transient_errors: impl Into<Cow<'static, [&'static str]>>,
80 ) -> Self {
81 self.transient_errors = Some(transient_errors.into());
82 self
83 }
84
85 pub fn build(self) -> AwsErrorCodeClassifier<E> {
87 AwsErrorCodeClassifier {
88 throttling_errors: self.throttling_errors.unwrap_or(THROTTLING_ERRORS.into()),
89 transient_errors: self.transient_errors.unwrap_or(TRANSIENT_ERRORS.into()),
90 _inner: self._inner,
91 }
92 }
93}
94
95impl<E> AwsErrorCodeClassifier<E> {
96 pub fn new() -> Self {
98 Self::default()
99 }
100
101 pub fn builder() -> AwsErrorCodeClassifierBuilder<E> {
103 AwsErrorCodeClassifierBuilder {
104 throttling_errors: None,
105 transient_errors: None,
106 _inner: PhantomData,
107 }
108 }
109}
110
111impl<E> ClassifyRetry for AwsErrorCodeClassifier<E>
112where
113 E: StdError + ProvideErrorMetadata + Send + Sync + 'static,
114{
115 fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction {
116 let output_or_error = ctx.output_or_error();
118 let error = match output_or_error {
120 Some(Ok(_)) | None => return RetryAction::NoActionIndicated,
121 Some(Err(err)) => err,
122 };
123
124 let retry_after = retry_after_from_ctx(ctx);
125
126 let error_code = OrchestratorError::as_operation_error(error)
127 .and_then(|err| err.downcast_ref::<E>())
128 .and_then(|err| err.code());
129
130 if let Some(error_code) = error_code {
131 if self.throttling_errors.contains(&error_code) {
132 return RetryAction::RetryIndicated(RetryReason::RetryableError {
133 kind: ErrorKind::ThrottlingError,
134 retry_after,
135 });
136 }
137 if self.transient_errors.contains(&error_code) {
138 return RetryAction::RetryIndicated(RetryReason::RetryableError {
139 kind: ErrorKind::TransientError,
140 retry_after,
141 });
142 }
143 };
144
145 RetryAction::NoActionIndicated
146 }
147
148 fn classify_retry_v2(&self, ctx: &InterceptorContext, previous: &RetryAction) -> RetryAction {
149 let own = self.classify_retry(ctx);
153 if own != RetryAction::NoActionIndicated {
154 return own;
155 }
156
157 if let RetryAction::RetryIndicated(RetryReason::RetryableError {
169 kind,
170 retry_after: None,
171 }) = previous
172 {
173 if let Some(retry_after) = retry_after_from_ctx(ctx) {
174 return RetryAction::RetryIndicated(RetryReason::RetryableError {
175 kind: *kind,
176 retry_after: Some(retry_after),
177 });
178 }
179 }
180
181 RetryAction::NoActionIndicated
182 }
183
184 fn name(&self) -> &'static str {
185 "AWS Error Code"
186 }
187
188 fn priority(&self) -> RetryClassifierPriority {
189 RetryClassifierPriority::run_before(
190 RetryClassifierPriority::modeled_as_retryable_classifier(),
191 )
192 }
193}
194
195#[cfg(test)]
196mod test {
197 use crate::retries::classifiers::AwsErrorCodeClassifier;
198 use aws_smithy_runtime_api::client::interceptors::context::InterceptorContext;
199 use aws_smithy_runtime_api::client::interceptors::context::{Error, Input};
200 use aws_smithy_runtime_api::client::orchestrator::OrchestratorError;
201 use aws_smithy_runtime_api::client::retries::classifiers::{ClassifyRetry, RetryAction};
202 use aws_smithy_types::body::SdkBody;
203 use aws_smithy_types::error::metadata::ProvideErrorMetadata;
204 use aws_smithy_types::error::ErrorMetadata;
205 use aws_smithy_types::retry::ErrorKind;
206 use std::fmt;
207 use std::time::Duration;
208
209 #[derive(Debug)]
210 struct CodedError {
211 metadata: ErrorMetadata,
212 }
213
214 impl CodedError {
215 fn new(code: &'static str) -> Self {
216 Self {
217 metadata: ErrorMetadata::builder().code(code).build(),
218 }
219 }
220 }
221
222 impl fmt::Display for CodedError {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 write!(f, "Coded Error")
225 }
226 }
227
228 impl std::error::Error for CodedError {}
229
230 impl ProvideErrorMetadata for CodedError {
231 fn meta(&self) -> &ErrorMetadata {
232 &self.metadata
233 }
234 }
235
236 #[test]
237 fn classify_by_error_code() {
238 let policy = AwsErrorCodeClassifier::<CodedError>::new();
239 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
240 ctx.set_output_or_error(Err(OrchestratorError::operation(Error::erase(
241 CodedError::new("Throttling"),
242 ))));
243
244 assert_eq!(policy.classify_retry(&ctx), RetryAction::throttling_error());
245
246 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
247 ctx.set_output_or_error(Err(OrchestratorError::operation(Error::erase(
248 CodedError::new("RequestTimeout"),
249 ))));
250 assert_eq!(policy.classify_retry(&ctx), RetryAction::transient_error())
251 }
252
253 #[test]
254 fn classify_generic() {
255 let policy = AwsErrorCodeClassifier::<ErrorMetadata>::new();
256 let err = ErrorMetadata::builder().code("SlowDown").build();
257 let test_response = http_1x::Response::new("OK").map(SdkBody::from);
258
259 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
260 ctx.set_response(test_response.try_into().unwrap());
261 ctx.set_output_or_error(Err(OrchestratorError::operation(Error::erase(err))));
262
263 assert_eq!(policy.classify_retry(&ctx), RetryAction::throttling_error());
264 }
265
266 #[test]
267 fn test_retry_after_header() {
268 let policy = AwsErrorCodeClassifier::<ErrorMetadata>::new();
269 let err = ErrorMetadata::builder().code("SlowDown").build();
270 let res = http_1x::Response::builder()
271 .header("x-amz-retry-after", "5000")
272 .body("retry later")
273 .unwrap()
274 .map(SdkBody::from);
275 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
276 ctx.set_response(res.try_into().unwrap());
277 ctx.set_output_or_error(Err(OrchestratorError::operation(Error::erase(err))));
278
279 assert_eq!(
280 policy.classify_retry(&ctx),
281 RetryAction::retryable_error_with_explicit_delay(
282 ErrorKind::ThrottlingError,
283 Duration::from_secs(5)
284 )
285 );
286 }
287
288 #[test]
289 fn test_invalid_retry_after_header_is_ignored() {
290 let policy = AwsErrorCodeClassifier::<ErrorMetadata>::new();
291 let err = ErrorMetadata::builder().code("SlowDown").build();
292 let res = http_1x::Response::builder()
293 .header("x-amz-retry-after", "invalid")
294 .body("retry later")
295 .unwrap()
296 .map(SdkBody::from);
297 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
298 ctx.set_response(res.try_into().unwrap());
299 ctx.set_output_or_error(Err(OrchestratorError::operation(Error::erase(err))));
300
301 assert_eq!(
304 policy.classify_retry(&ctx),
305 RetryAction::retryable_error(ErrorKind::ThrottlingError)
306 );
307 }
308
309 fn ctx_with(
314 retry_after_header_millis: Option<&str>,
315 error_code: Option<&'static str>,
316 ) -> InterceptorContext {
317 let mut builder = http_1x::Response::builder();
318 if let Some(millis) = retry_after_header_millis {
319 builder = builder.header("x-amz-retry-after", millis);
320 }
321 let res = builder.body("nope").unwrap().map(SdkBody::from);
322
323 let err = match error_code {
324 Some(code) => ErrorMetadata::builder().code(code).build(),
325 None => ErrorMetadata::builder().build(),
326 };
327
328 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
329 ctx.set_response(res.try_into().unwrap());
330 ctx.set_output_or_error(Err(OrchestratorError::operation(Error::erase(err))));
331 ctx
332 }
333
334 #[test]
342 fn classify_retry_v2_attaches_server_delay_on_bare_5xx() {
343 let policy = AwsErrorCodeClassifier::<ErrorMetadata>::new();
344 let ctx = ctx_with(Some("5000"), None);
346
347 let previous = RetryAction::transient_error();
348 assert_eq!(
349 policy.classify_retry_v2(&ctx, &previous),
350 RetryAction::retryable_error_with_explicit_delay(
351 ErrorKind::TransientError,
352 Duration::from_secs(5)
353 ),
354 );
355 }
356
357 #[test]
361 fn classify_retry_v2_attaches_server_delay_on_unrecognized_error_code() {
362 let policy = AwsErrorCodeClassifier::<ErrorMetadata>::new();
363 let ctx = ctx_with(Some("5000"), Some("InternalError"));
364
365 let previous = RetryAction::transient_error();
366 assert_eq!(
367 policy.classify_retry_v2(&ctx, &previous),
368 RetryAction::retryable_error_with_explicit_delay(
369 ErrorKind::TransientError,
370 Duration::from_secs(5)
371 ),
372 );
373 }
374
375 #[test]
378 fn classify_retry_v2_preserves_previous_kind() {
379 let policy = AwsErrorCodeClassifier::<ErrorMetadata>::new();
380 let ctx = ctx_with(Some("2500"), None);
381
382 let previous = RetryAction::retryable_error(ErrorKind::ServerError);
383 assert_eq!(
384 policy.classify_retry_v2(&ctx, &previous),
385 RetryAction::retryable_error_with_explicit_delay(
386 ErrorKind::ServerError,
387 Duration::from_millis(2500)
388 ),
389 );
390 }
391
392 #[test]
397 fn classify_retry_v2_prefers_own_error_code_verdict() {
398 let policy = AwsErrorCodeClassifier::<ErrorMetadata>::new();
399 let ctx = ctx_with(Some("5000"), Some("SlowDown"));
400
401 let previous = RetryAction::transient_error();
403 assert_eq!(
404 policy.classify_retry_v2(&ctx, &previous),
405 RetryAction::retryable_error_with_explicit_delay(
406 ErrorKind::ThrottlingError,
407 Duration::from_secs(5)
408 ),
409 );
410 }
411
412 #[test]
416 fn classify_retry_v2_ignores_header_without_retryable_previous() {
417 let policy = AwsErrorCodeClassifier::<ErrorMetadata>::new();
418 let ctx = ctx_with(Some("5000"), None);
419
420 assert_eq!(
421 policy.classify_retry_v2(&ctx, &RetryAction::NoActionIndicated),
422 RetryAction::NoActionIndicated,
423 );
424 }
425
426 #[test]
435 fn classify_retry_v2_abstains_when_previous_has_explicit_delay() {
436 let policy = AwsErrorCodeClassifier::<ErrorMetadata>::new();
437 let ctx = ctx_with(Some("5000"), None);
438
439 let previous = RetryAction::retryable_error_with_explicit_delay(
440 ErrorKind::TransientError,
441 Duration::from_secs(1),
442 );
443 assert_eq!(
444 policy.classify_retry_v2(&ctx, &previous),
445 RetryAction::NoActionIndicated,
446 );
447 }
448
449 #[test]
454 fn classify_retry_v2_falls_back_without_valid_header() {
455 let policy = AwsErrorCodeClassifier::<ErrorMetadata>::new();
456
457 let previous = RetryAction::transient_error();
458 assert_eq!(
460 policy.classify_retry_v2(&ctx_with(None, None), &previous),
461 RetryAction::NoActionIndicated,
462 );
463 assert_eq!(
465 policy.classify_retry_v2(&ctx_with(Some("invalid"), None), &previous),
466 RetryAction::NoActionIndicated,
467 );
468 }
469}