Skip to content

Commit bcfca5a

Browse files
authored
Merge pull request #314 from http-rs/retry-after
Add the `Retry-After` type header
2 parents b58b360 + dc1f718 commit bcfca5a

File tree

2 files changed

+172
-0
lines changed

2 files changed

+172
-0
lines changed

Diff for: src/other/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
mod date;
44
mod expect;
55
mod referer;
6+
mod retry_after;
67
mod source_map;
78

89
pub use date::Date;
910
pub use expect::Expect;
1011
pub use referer::Referer;
12+
pub use retry_after::RetryAfter;
1113
pub use source_map::SourceMap;

Diff for: src/other/retry_after.rs

+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
use std::time::{Duration, SystemTime, SystemTimeError};
2+
3+
use crate::headers::{HeaderName, HeaderValue, Headers, RETRY_AFTER};
4+
use crate::utils::{fmt_http_date, parse_http_date};
5+
6+
/// Indicate how long the user agent should wait before making a follow-up request.
7+
///
8+
/// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After)
9+
///
10+
/// # Specifications
11+
///
12+
/// - [RFC 7231, section 3.1.4.2: Retry-After](https://tools.ietf.org/html/rfc7231#section-3.1.4.2)
13+
///
14+
/// # Examples
15+
///
16+
/// ```no_run
17+
/// # fn main() -> http_types::Result<()> {
18+
/// #
19+
/// use http_types::other::RetryAfter;
20+
/// use http_types::Response;
21+
/// use std::time::{SystemTime, Duration};
22+
/// use async_std::task;
23+
///
24+
/// let retry = RetryAfter::new(Duration::from_secs(10));
25+
///
26+
/// let mut headers = Response::new(429);
27+
/// retry.apply(&mut headers);
28+
///
29+
/// // Sleep for the duration, then try the task again.
30+
/// let retry = RetryAfter::from_headers(headers)?.unwrap();
31+
/// task::sleep(retry.duration_since(SystemTime::now())?);
32+
/// #
33+
/// # Ok(()) }
34+
/// ```
35+
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
36+
pub struct RetryAfter {
37+
inner: RetryDirective,
38+
}
39+
40+
#[allow(clippy::len_without_is_empty)]
41+
impl RetryAfter {
42+
/// Create a new instance from a `Duration`.
43+
///
44+
/// This value will be encoded over the wire as a relative offset in seconds.
45+
pub fn new(dur: Duration) -> Self {
46+
Self {
47+
inner: RetryDirective::Duration(dur),
48+
}
49+
}
50+
51+
/// Create a new instance from a `SystemTime` instant.
52+
///
53+
/// This value will be encoded a specific `Date` over the wire.
54+
pub fn new_at(at: SystemTime) -> Self {
55+
Self {
56+
inner: RetryDirective::SystemTime(at),
57+
}
58+
}
59+
60+
/// Create a new instance from headers.
61+
pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
62+
let header = match headers.as_ref().get(RETRY_AFTER) {
63+
Some(headers) => headers.last(),
64+
None => return Ok(None),
65+
};
66+
67+
let inner = match header.as_str().parse::<u64>() {
68+
Ok(dur) => RetryDirective::Duration(Duration::from_secs(dur)),
69+
Err(_) => {
70+
let at = parse_http_date(header.as_str())?;
71+
RetryDirective::SystemTime(at)
72+
}
73+
};
74+
Ok(Some(Self { inner }))
75+
}
76+
77+
/// Returns the amount of time elapsed from an earlier point in time.
78+
///
79+
/// # Errors
80+
///
81+
/// This may return an error if the `earlier` time was after the current time.
82+
pub fn duration_since(&self, earlier: SystemTime) -> Result<Duration, SystemTimeError> {
83+
let at = match self.inner {
84+
RetryDirective::Duration(dur) => SystemTime::now() + dur,
85+
RetryDirective::SystemTime(at) => at,
86+
};
87+
88+
at.duration_since(earlier)
89+
}
90+
91+
/// Sets the header.
92+
pub fn apply(&self, mut headers: impl AsMut<Headers>) {
93+
headers.as_mut().insert(self.name(), self.value());
94+
}
95+
96+
/// Get the `HeaderName`.
97+
pub fn name(&self) -> HeaderName {
98+
RETRY_AFTER
99+
}
100+
101+
/// Get the `HeaderValue`.
102+
pub fn value(&self) -> HeaderValue {
103+
let output = match self.inner {
104+
RetryDirective::Duration(dur) => format!("{}", dur.as_secs()),
105+
RetryDirective::SystemTime(at) => fmt_http_date(at),
106+
};
107+
// SAFETY: the internal string is validated to be ASCII.
108+
unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
109+
}
110+
}
111+
112+
impl Into<SystemTime> for RetryAfter {
113+
fn into(self) -> SystemTime {
114+
match self.inner {
115+
RetryDirective::Duration(dur) => SystemTime::now() + dur,
116+
RetryDirective::SystemTime(at) => at,
117+
}
118+
}
119+
}
120+
121+
/// What value are we decoding into?
122+
///
123+
/// This value is intionally never exposes; all end-users want is a `Duration`
124+
/// value that tells them how long to wait for before trying again.
125+
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
126+
enum RetryDirective {
127+
Duration(Duration),
128+
SystemTime(SystemTime),
129+
}
130+
131+
#[cfg(test)]
132+
mod test {
133+
use super::*;
134+
use crate::headers::Headers;
135+
136+
#[test]
137+
fn smoke() -> crate::Result<()> {
138+
let retry = RetryAfter::new(Duration::from_secs(10));
139+
140+
let mut headers = Headers::new();
141+
retry.apply(&mut headers);
142+
143+
// `SystemTime::now` uses sub-second precision which means there's some
144+
// offset that's not encoded.
145+
let now = SystemTime::now();
146+
let retry = RetryAfter::from_headers(headers)?.unwrap();
147+
assert_eq!(
148+
retry.duration_since(now)?.as_secs(),
149+
Duration::from_secs(10).as_secs()
150+
);
151+
Ok(())
152+
}
153+
154+
#[test]
155+
fn new_at() -> crate::Result<()> {
156+
let now = SystemTime::now();
157+
let retry = RetryAfter::new_at(now + Duration::from_secs(10));
158+
159+
let mut headers = Headers::new();
160+
retry.apply(&mut headers);
161+
162+
// `SystemTime::now` uses sub-second precision which means there's some
163+
// offset that's not encoded.
164+
let retry = RetryAfter::from_headers(headers)?.unwrap();
165+
let delta = retry.duration_since(now)?;
166+
assert!(delta >= Duration::from_secs(9));
167+
assert!(delta <= Duration::from_secs(10));
168+
Ok(())
169+
}
170+
}

0 commit comments

Comments
 (0)