http_types/other/
retry_after.rs

1use std::time::{Duration, SystemTime, SystemTimeError};
2
3use crate::headers::{HeaderName, HeaderValue, Headers, RETRY_AFTER};
4use 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)]
36pub struct RetryAfter {
37    inner: RetryDirective,
38}
39
40#[allow(clippy::len_without_is_empty)]
41impl 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
112impl From<RetryAfter> for SystemTime {
113    fn from(retry_after: RetryAfter) -> Self {
114        match retry_after.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)]
126enum RetryDirective {
127    Duration(Duration),
128    SystemTime(SystemTime),
129}
130
131#[cfg(test)]
132mod 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}