Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the Retry-After type header #314

Merged
merged 3 commits into from
Jan 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/other/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
mod date;
mod expect;
mod referer;
mod retry_after;
mod source_map;

pub use date::Date;
pub use expect::Expect;
pub use referer::Referer;
pub use retry_after::RetryAfter;
pub use source_map::SourceMap;
170 changes: 170 additions & 0 deletions src/other/retry_after.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use std::time::{Duration, SystemTime, SystemTimeError};

use crate::headers::{HeaderName, HeaderValue, Headers, RETRY_AFTER};
use crate::utils::{fmt_http_date, parse_http_date};

/// Indicate how long the user agent should wait before making a follow-up request.
///
/// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After)
///
/// # Specifications
///
/// - [RFC 7231, section 3.1.4.2: Retry-After](https://tools.ietf.org/html/rfc7231#section-3.1.4.2)
///
/// # Examples
///
/// ```no_run
/// # fn main() -> http_types::Result<()> {
/// #
/// use http_types::other::RetryAfter;
/// use http_types::Response;
/// use std::time::{SystemTime, Duration};
/// use async_std::task;
///
/// let retry = RetryAfter::new(Duration::from_secs(10));
///
/// let mut headers = Response::new(429);
/// retry.apply(&mut headers);
///
/// // Sleep for the duration, then try the task again.
/// let retry = RetryAfter::from_headers(headers)?.unwrap();
/// task::sleep(retry.duration_since(SystemTime::now())?);
/// #
/// # Ok(()) }
/// ```
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct RetryAfter {
inner: RetryDirective,
}

#[allow(clippy::len_without_is_empty)]
impl RetryAfter {
/// Create a new instance from a `Duration`.
///
/// This value will be encoded over the wire as a relative offset in seconds.
pub fn new(dur: Duration) -> Self {
Self {
inner: RetryDirective::Duration(dur),
}
}

/// Create a new instance from a `SystemTime` instant.
///
/// This value will be encoded a specific `Date` over the wire.
pub fn new_at(at: SystemTime) -> Self {
Self {
inner: RetryDirective::SystemTime(at),
}
}

/// Create a new instance from headers.
pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
let header = match headers.as_ref().get(RETRY_AFTER) {
Some(headers) => headers.last(),
None => return Ok(None),
};

let inner = match header.as_str().parse::<u64>() {
Ok(dur) => RetryDirective::Duration(Duration::from_secs(dur)),
Err(_) => {
let at = parse_http_date(header.as_str())?;
RetryDirective::SystemTime(at)
}
};
Ok(Some(Self { inner }))
}

/// Returns the amount of time elapsed from an earlier point in time.
///
/// # Errors
///
/// This may return an error if the `earlier` time was after the current time.
pub fn duration_since(&self, earlier: SystemTime) -> Result<Duration, SystemTimeError> {
let at = match self.inner {
RetryDirective::Duration(dur) => SystemTime::now() + dur,
RetryDirective::SystemTime(at) => at,
};

at.duration_since(earlier)
}

/// Sets the header.
pub fn apply(&self, mut headers: impl AsMut<Headers>) {
headers.as_mut().insert(self.name(), self.value());
}

/// Get the `HeaderName`.
pub fn name(&self) -> HeaderName {
RETRY_AFTER
}

/// Get the `HeaderValue`.
pub fn value(&self) -> HeaderValue {
let output = match self.inner {
RetryDirective::Duration(dur) => format!("{}", dur.as_secs()),
RetryDirective::SystemTime(at) => fmt_http_date(at),
};
// SAFETY: the internal string is validated to be ASCII.
unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
}
}

impl Into<SystemTime> for RetryAfter {
fn into(self) -> SystemTime {
match self.inner {
RetryDirective::Duration(dur) => SystemTime::now() + dur,
RetryDirective::SystemTime(at) => at,
}
}
}

/// What value are we decoding into?
///
/// This value is intionally never exposes; all end-users want is a `Duration`
/// value that tells them how long to wait for before trying again.
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
enum RetryDirective {
Duration(Duration),
SystemTime(SystemTime),
}

#[cfg(test)]
mod test {
use super::*;
use crate::headers::Headers;

#[test]
fn smoke() -> crate::Result<()> {
let retry = RetryAfter::new(Duration::from_secs(10));

let mut headers = Headers::new();
retry.apply(&mut headers);

// `SystemTime::now` uses sub-second precision which means there's some
// offset that's not encoded.
let now = SystemTime::now();
let retry = RetryAfter::from_headers(headers)?.unwrap();
assert_eq!(
retry.duration_since(now)?.as_secs(),
Duration::from_secs(10).as_secs()
);
Ok(())
}

#[test]
fn new_at() -> crate::Result<()> {
let now = SystemTime::now();
let retry = RetryAfter::new_at(now + Duration::from_secs(10));

let mut headers = Headers::new();
retry.apply(&mut headers);

// `SystemTime::now` uses sub-second precision which means there's some
// offset that's not encoded.
let retry = RetryAfter::from_headers(headers)?.unwrap();
let delta = retry.duration_since(now)?;
assert!(delta >= Duration::from_secs(9));
assert!(delta <= Duration::from_secs(10));
Ok(())
}
}