Skip to content

Commit 5b0b25a

Browse files
authored
Retry policy returns non-retriable response instead of error (#2834)
1 parent c5a7db2 commit 5b0b25a

File tree

2 files changed

+92
-25
lines changed

2 files changed

+92
-25
lines changed

sdk/typespec/typespec_client_core/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
### Breaking Changes
88

9+
- When a retry policy receives a response whose status code indicates the policy shouldn't retry the request, it now returns that response instead of an error
10+
911
### Bugs Fixed
1012

1113
### Other Changes

sdk/typespec/typespec_client_core/src/http/policies/retry/mod.rs

Lines changed: 90 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -149,18 +149,24 @@ where
149149
// only start keeping track of time after the first request is made
150150
let start = start.get_or_insert_with(OffsetDateTime::now_utc);
151151
let (last_error, retry_after) = match result {
152-
Ok(response) if response.status().is_success() => {
153-
trace!(
154-
?request,
155-
?response,
156-
"server returned success status {}",
157-
response.status(),
158-
);
159-
return Ok(response);
160-
}
161152
Ok(response) => {
162-
// Error status code
163153
let status = response.status();
154+
if !RETRY_STATUSES.contains(&status) {
155+
if status.is_success() {
156+
trace!(
157+
?request,
158+
?response,
159+
"server returned success status {}",
160+
status,
161+
);
162+
} else {
163+
debug!(
164+
"server returned status which will not be retried: {}",
165+
status
166+
);
167+
}
168+
return Ok(response);
169+
}
164170

165171
// For a 429 response (TooManyRequests) or 503 (ServiceUnavailable),
166172
// use any "retry-after" headers returned by the server to determine how long to wait before retrying.
@@ -179,21 +185,6 @@ where
179185
http_error.error_code().map(std::borrow::ToOwned::to_owned),
180186
);
181187

182-
if !RETRY_STATUSES.contains(&status) {
183-
debug!(
184-
"server returned error status which will not be retried: {}",
185-
status
186-
);
187-
// Server didn't return a status we retry on so return early
188-
let error = Error::full(
189-
error_kind,
190-
http_error,
191-
format!(
192-
"server returned error status which will not be retried: {status}"
193-
),
194-
);
195-
return Err(error);
196-
}
197188
debug!(
198189
"server returned error status which requires retry: {}",
199190
status
@@ -233,7 +224,28 @@ where
233224
#[cfg(test)]
234225
mod test {
235226
use super::*;
227+
use crate::http::{
228+
headers::Headers, Context, FixedRetryOptions, Method, RawResponse, Request, RetryOptions,
229+
Url,
230+
};
236231
use ::time::macros::datetime;
232+
use std::sync::{Arc, Mutex};
233+
234+
// Policy that counts the requests it receives and returns responses having a given status code
235+
#[derive(Debug)]
236+
struct StatusResponder {
237+
request_count: Arc<Mutex<u32>>,
238+
status: StatusCode,
239+
}
240+
241+
#[async_trait]
242+
impl Policy for StatusResponder {
243+
async fn send(&self, _: &Context, _: &mut Request, _: &[Arc<dyn Policy>]) -> PolicyResult {
244+
let mut count = self.request_count.lock().unwrap();
245+
*count += 1;
246+
Ok(RawResponse::from_bytes(self.status, Headers::new(), ""))
247+
}
248+
}
237249

238250
// A function that returns a fixed "now" value for testing.
239251
fn datetime_now() -> OffsetDateTime {
@@ -290,4 +302,57 @@ mod test {
290302
let retry_after = get_retry_after(&headers, datetime_now);
291303
assert_eq!(retry_after, Some(Duration::seconds(123)));
292304
}
305+
306+
#[tokio::test]
307+
async fn test_retry_statuses() {
308+
let retries = 2u32;
309+
let retry_policy = RetryOptions::fixed(
310+
FixedRetryOptions::default()
311+
.delay(Duration::nanoseconds(1))
312+
.max_retries(retries),
313+
)
314+
.to_policy();
315+
let ctx = Context::new();
316+
let url = Url::parse("http://localhost").unwrap();
317+
318+
for &status in RETRY_STATUSES {
319+
let mut request = Request::new(url.clone(), Method::Get);
320+
let count = Arc::new(Mutex::new(0));
321+
let mock = StatusResponder {
322+
request_count: count.clone(),
323+
status,
324+
};
325+
let next = vec![Arc::new(mock) as Arc<dyn Policy>];
326+
327+
retry_policy
328+
.send(&ctx, &mut request, &next)
329+
.await
330+
.expect_err("Policy should return an error after exhausting retries");
331+
332+
assert_eq!(
333+
retries + 1,
334+
*count.lock().unwrap(),
335+
"Policy should retry {status}"
336+
);
337+
}
338+
339+
let mut request = Request::new(url.clone(), Method::Get);
340+
let count = Arc::new(Mutex::new(0));
341+
let next = vec![Arc::new(StatusResponder {
342+
request_count: count.clone(),
343+
status: StatusCode::Unauthorized,
344+
}) as Arc<dyn Policy>];
345+
346+
let response = retry_policy
347+
.send(&ctx, &mut request, &next)
348+
.await
349+
.expect("Policy should return a response whose status isn't in RETRY_STATUSES");
350+
351+
assert_eq!(response.status(), StatusCode::Unauthorized);
352+
assert_eq!(
353+
1,
354+
*count.lock().unwrap(),
355+
"Policy shouldn't retry after receiving a response whose status isn't in RETRY_STATUSES"
356+
);
357+
}
293358
}

0 commit comments

Comments
 (0)