diff --git a/CHANGELOG.md b/CHANGELOG.md index 05f59c2..0cf2d9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/error.rs b/src/error.rs index 8d66717..88652df 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use crate::resource::{ErrorResponse, OAuth2ErrorResponse}; use reqwest::StatusCode; use thiserror::Error; @@ -25,18 +27,28 @@ enum ErrorKind { ErrorResponse { status: StatusCode, response: ErrorResponse, + retry_after: Option, }, #[error("OAuth2 error with {status}: ({}) {}", .response.error, .response.error_description)] OAuth2Error { status: StatusCode, response: OAuth2ErrorResponse, + retry_after: Option, }, } 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, + ) -> Self { Self { - inner: Box::new(ErrorKind::ErrorResponse { status, response }), + inner: Box::new(ErrorKind::ErrorResponse { + status, + response, + retry_after, + }), } } @@ -49,9 +61,14 @@ impl Error { pub(crate) fn from_oauth2_error_response( status: StatusCode, response: OAuth2ErrorResponse, + retry_after: Option, ) -> Self { Self { - inner: Box::new(ErrorKind::OAuth2Error { status, response }), + inner: Box::new(ErrorKind::OAuth2Error { + status, + response, + retry_after, + }), } } @@ -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: + #[must_use] + pub fn retry_after(&self) -> Option { + match &*self.inner { + ErrorKind::ErrorResponse { retry_after, .. } + | ErrorKind::OAuth2Error { retry_after, .. } => { + Some(Duration::from_secs((*retry_after)?.into())) + } + _ => None, + } + } } impl From for Error { diff --git a/src/util.rs b/src/util.rs index 8f397da..5f9e304 100644 --- a/src/util.rs +++ b/src/util.rs @@ -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; @@ -310,8 +310,9 @@ pub(crate) async fn handle_error_response(resp: Response) -> Result { 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)) } } @@ -320,7 +321,21 @@ pub(crate) async fn handle_oauth2_error_response(resp: Response) -> Result. +/// And HTTP requires it to be a non-negative integer: +/// . +fn parse_retry_after_sec(resp: &Response) -> Option { + resp.headers() + .get(header::RETRY_AFTER)? + .to_str() + .ok()? + .parse() + .ok() +}