Skip to content

Commit

Permalink
Parse and save Retry-After in errors
Browse files Browse the repository at this point in the history
Closes #7.
  • Loading branch information
oxalica committed Mar 23, 2024
1 parent b932c7c commit 247972b
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 6 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
- Configurable `Tenant` for `Auth`, this allows login into applications that
accept only personal accounts.
- Expose `Auth::client`.
- `Retry-After` response header on rate limited or service unavailability is
now parsed and returned via `Error::retry_after`.

### Changed

Expand Down
38 changes: 35 additions & 3 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::time::Duration;

use crate::resource::{ErrorResponse, OAuth2ErrorResponse};
use reqwest::StatusCode;
use thiserror::Error;
Expand Down Expand Up @@ -25,18 +27,28 @@ enum ErrorKind {
ErrorResponse {
status: StatusCode,
response: ErrorResponse,
retry_after: Option<u32>,
},
#[error("OAuth2 error with {status}: ({}) {}", .response.error, .response.error_description)]
OAuth2Error {
status: StatusCode,
response: OAuth2ErrorResponse,
retry_after: Option<u32>,
},
}

impl Error {
pub(crate) fn from_error_response(status: StatusCode, response: ErrorResponse) -> Self {
pub(crate) fn from_error_response(
status: StatusCode,
response: ErrorResponse,
retry_after: Option<u32>,
) -> Self {
Self {
inner: Box::new(ErrorKind::ErrorResponse { status, response }),
inner: Box::new(ErrorKind::ErrorResponse {
status,
response,
retry_after,
}),
}
}

Expand All @@ -49,9 +61,14 @@ impl Error {
pub(crate) fn from_oauth2_error_response(
status: StatusCode,
response: OAuth2ErrorResponse,
retry_after: Option<u32>,
) -> Self {
Self {
inner: Box::new(ErrorKind::OAuth2Error { status, response }),
inner: Box::new(ErrorKind::OAuth2Error {
status,
response,
retry_after,
}),
}
}

Expand Down Expand Up @@ -84,6 +101,21 @@ impl Error {
}
}
}

/// Get the retry delay hint on rate limited (HTTP 429) or server unavailability, if any.
///
/// This is parsed from response header `Retry-After`.
/// See: <https://learn.microsoft.com/en-us/graph/throttling>
#[must_use]
pub fn retry_after(&self) -> Option<Duration> {
match &*self.inner {
ErrorKind::ErrorResponse { retry_after, .. }
| ErrorKind::OAuth2Error { retry_after, .. } => {
Some(Duration::from_secs((*retry_after)?.into()))
}
_ => None,
}
}
}

impl From<reqwest::Error> for Error {
Expand Down
21 changes: 18 additions & 3 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::{
error::{Error, Result},
resource::{DriveId, ErrorResponse, ItemId, OAuth2ErrorResponse},
};
use reqwest::{RequestBuilder, Response, StatusCode};
use reqwest::{header, RequestBuilder, Response, StatusCode};
use serde::{de, Deserialize};
use url::PathSegmentsMut;

Expand Down Expand Up @@ -310,8 +310,9 @@ pub(crate) async fn handle_error_response(resp: Response) -> Result<Response> {
if status.is_success() || status.is_redirection() {
Ok(resp)
} else {
let retry_after = parse_retry_after_sec(&resp);
let resp: Resp = resp.json().await?;
Err(Error::from_error_response(status, resp.error))
Err(Error::from_error_response(status, resp.error, retry_after))
}
}

Expand All @@ -320,7 +321,21 @@ pub(crate) async fn handle_oauth2_error_response(resp: Response) -> Result<Respo
if status.is_success() {
Ok(resp)
} else {
let retry_after = parse_retry_after_sec(&resp);
let resp: OAuth2ErrorResponse = resp.json().await?;
Err(Error::from_oauth2_error_response(status, resp))
Err(Error::from_oauth2_error_response(status, resp, retry_after))
}
}

/// The documentation said it is in seconds:
/// <https://learn.microsoft.com/en-us/graph/throttling#best-practices-to-handle-throttling>.
/// And HTTP requires it to be a non-negative integer:
/// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After>.
fn parse_retry_after_sec(resp: &Response) -> Option<u32> {
resp.headers()
.get(header::RETRY_AFTER)?
.to_str()
.ok()?
.parse()
.ok()
}

0 comments on commit 247972b

Please sign in to comment.