Skip to content

Commit aa773bb

Browse files
committed
Update retry_after.rs
1 parent b4562d9 commit aa773bb

File tree

1 file changed

+84
-57
lines changed

1 file changed

+84
-57
lines changed

src/other/retry_after.rs

Lines changed: 84 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
use std::time::Duration;
2-
use std::{convert::TryInto, str::FromStr};
1+
use std::time::{Duration, SystemTime, SystemTimeError};
32

43
use crate::headers::{HeaderName, HeaderValue, Headers, RETRY_AFTER};
4+
use crate::utils::{fmt_http_date, parse_http_date};
55

6-
/// Indicate an alternate location for the returned data
6+
/// Indicate how long the user agent should wait before making a follow-up request.
77
///
88
/// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After)
99
///
@@ -13,73 +13,80 @@ use crate::headers::{HeaderName, HeaderValue, Headers, RETRY_AFTER};
1313
///
1414
/// # Examples
1515
///
16-
/// ```
16+
/// ```no_run
1717
/// # fn main() -> http_types::Result<()> {
1818
/// #
1919
/// use http_types::other::RetryAfter;
20-
/// use http_types::{Response, Duration};
20+
/// use http_types::Response;
21+
/// use std::time::{SystemTime, Duration};
22+
/// use async_std::task;
2123
///
22-
/// let loc = RetryAfter::new(Duration::parse("https://example.com/foo/bar")?);
24+
/// let now = SystemTime::now();
25+
/// let retry = RetryAfter::new_at(now + Duration::from_secs(10));
2326
///
24-
/// let mut res = Response::new(200);
25-
/// loc.apply(&mut res);
27+
/// let mut headers = Response::new(429);
28+
/// retry.apply(&mut headers);
2629
///
27-
/// let base_url = Duration::parse("https://example.com")?;
28-
/// let loc = RetryAfter::from_headers(base_url, res)?.unwrap();
29-
/// assert_eq!(
30-
/// loc.value(),
31-
/// Duration::parse("https://example.com/foo/bar")?.as_str()
32-
/// );
30+
/// // Sleep for the duration, then try the task again.
31+
/// let retry = RetryAfter::from_headers(headers)?.unwrap();
32+
/// task::sleep(retry.duration_since(now)?);
3333
/// #
3434
/// # Ok(()) }
3535
/// ```
36-
#[derive(Debug)]
36+
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
3737
pub struct RetryAfter {
38-
dur: Duration,
38+
inner: RetryDirective,
3939
}
4040

4141
#[allow(clippy::len_without_is_empty)]
4242
impl RetryAfter {
43-
/// Create a new instance.
43+
/// Create a new instance from a `Duration`.
44+
///
45+
/// This value will be encoded over the wire as a relative offset in seconds.
4446
pub fn new(dur: Duration) -> Self {
4547
Self {
46-
dur: location
47-
.try_into()
48-
.expect("could not convert into a valid URL"),
48+
inner: RetryDirective::Duration(dur),
4949
}
5050
}
5151

52-
/// Create a new instance from headers.
52+
/// Create a new instance from a `SystemTime` instant.
5353
///
54-
/// `Retry-After` headers can provide both full and partial URLs. In
55-
/// order to always return fully qualified URLs, a base URL must be passed to
56-
/// reference the current environment. In HTTP/1.1 and above this value can
57-
/// always be determined from the request.
58-
pub fn from_headers<U>(base_url: U, headers: impl AsRef<Headers>) -> crate::Result<Option<Self>>
59-
where
60-
U: TryInto<Duration>,
61-
U::Error: std::fmt::Debug,
62-
{
63-
let headers = match headers.as_ref().get(RETRY_AFTER) {
64-
Some(headers) => headers,
54+
/// This value will be encoded a specific `Date` over the wire.
55+
pub fn new_at(at: SystemTime) -> Self {
56+
Self {
57+
inner: RetryDirective::SystemTime(at),
58+
}
59+
}
60+
61+
/// Create a new instance from headers.
62+
pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
63+
let header = match headers.as_ref().get(RETRY_AFTER) {
64+
Some(headers) => headers.last(),
6565
None => return Ok(None),
6666
};
6767

68-
// If we successfully parsed the header then there's always at least one
69-
// entry. We want the last entry.
70-
let location = headers.iter().last().unwrap();
71-
72-
let location = match Duration::from_str(location.as_str()) {
73-
Ok(url) => url,
68+
let inner = match header.as_str().parse::<u64>() {
69+
Ok(dur) => RetryDirective::Duration(Duration::from_secs(dur)),
7470
Err(_) => {
75-
let base_url = base_url
76-
.try_into()
77-
.expect("Could not convert base_url into a valid URL");
78-
let url = base_url.join(location.as_str())?;
79-
url
71+
let at = parse_http_date(header.as_str())?;
72+
RetryDirective::SystemTime(at)
8073
}
8174
};
82-
Ok(Some(Self { dur: location }))
75+
Ok(Some(Self { inner }))
76+
}
77+
78+
/// Returns the amount of time elapsed from an earlier point in time.
79+
///
80+
/// # Errors
81+
///
82+
/// This may return an error if the `earlier` time was after the current time.
83+
pub fn duration_since(&self, earlier: SystemTime) -> Result<Duration, SystemTimeError> {
84+
let at = match self.inner {
85+
RetryDirective::Duration(dur) => SystemTime::now() + dur,
86+
RetryDirective::SystemTime(at) => at,
87+
};
88+
89+
at.duration_since(earlier)
8390
}
8491

8592
/// Sets the header.
@@ -94,33 +101,53 @@ impl RetryAfter {
94101

95102
/// Get the `HeaderValue`.
96103
pub fn value(&self) -> HeaderValue {
97-
let output = format!("{}", self.dur);
104+
let output = match self.inner {
105+
RetryDirective::Duration(dur) => format!("{}", dur.as_secs()),
106+
RetryDirective::SystemTime(at) => fmt_http_date(at),
107+
};
98108
// SAFETY: the internal string is validated to be ASCII.
99109
unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
100110
}
101111
}
102112

113+
impl Into<SystemTime> for RetryAfter {
114+
fn into(self) -> SystemTime {
115+
match self.inner {
116+
RetryDirective::Duration(dur) => SystemTime::now() + dur,
117+
RetryDirective::SystemTime(at) => at,
118+
}
119+
}
120+
}
121+
103122
#[cfg(test)]
104123
mod test {
105124
use super::*;
106125
use crate::headers::Headers;
107126

108-
// NOTE(yosh): I couldn't get a 400 test in because I couldn't generate any
109-
// invalid URLs. By default they get escaped, so ehhh -- I think it's fine.
110-
111127
#[test]
112128
fn smoke() -> crate::Result<()> {
113-
let loc = RetryAfter::new(Duration::parse("https://example.com/foo/bar")?);
129+
let now = SystemTime::now();
130+
let retry = RetryAfter::new_at(now + Duration::from_secs(10));
114131

115132
let mut headers = Headers::new();
116-
loc.apply(&mut headers);
117-
118-
let base_url = Duration::parse("https://example.com")?;
119-
let loc = RetryAfter::from_headers(base_url, headers)?.unwrap();
120-
assert_eq!(
121-
loc.value(),
122-
Duration::parse("https://example.com/foo/bar")?.as_str()
123-
);
133+
retry.apply(&mut headers);
134+
135+
// `SystemTime::now` uses sub-second precision which means there's some
136+
// offset that's not encoded.
137+
let retry = RetryAfter::from_headers(headers)?.unwrap();
138+
let delta = retry.duration_since(now)?;
139+
assert!(delta >= Duration::from_secs(9));
140+
assert!(delta <= Duration::from_secs(10));
124141
Ok(())
125142
}
126143
}
144+
145+
/// What value are we decoding into?
146+
///
147+
/// This value is intionally never exposes; all end-users want is a `Duration`
148+
/// value that tells them how long to wait for before trying again.
149+
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
150+
enum RetryDirective {
151+
Duration(Duration),
152+
SystemTime(SystemTime),
153+
}

0 commit comments

Comments
 (0)