1
- use std:: time:: Duration ;
2
- use std:: { convert:: TryInto , str:: FromStr } ;
1
+ use std:: time:: { Duration , SystemTime , SystemTimeError } ;
3
2
4
3
use crate :: headers:: { HeaderName , HeaderValue , Headers , RETRY_AFTER } ;
4
+ use crate :: utils:: { fmt_http_date, parse_http_date} ;
5
5
6
- /// Indicate an alternate location for the returned data
6
+ /// Indicate how long the user agent should wait before making a follow-up request.
7
7
///
8
8
/// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After)
9
9
///
@@ -13,73 +13,80 @@ use crate::headers::{HeaderName, HeaderValue, Headers, RETRY_AFTER};
13
13
///
14
14
/// # Examples
15
15
///
16
- /// ```
16
+ /// ```no_run
17
17
/// # fn main() -> http_types::Result<()> {
18
18
/// #
19
19
/// 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;
21
23
///
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));
23
26
///
24
- /// let mut res = Response::new(200 );
25
- /// loc .apply(&mut res );
27
+ /// let mut headers = Response::new(429 );
28
+ /// retry .apply(&mut headers );
26
29
///
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)?);
33
33
/// #
34
34
/// # Ok(()) }
35
35
/// ```
36
- #[ derive( Debug ) ]
36
+ #[ derive( Debug , Clone , Hash , PartialEq , Eq , PartialOrd , Ord ) ]
37
37
pub struct RetryAfter {
38
- dur : Duration ,
38
+ inner : RetryDirective ,
39
39
}
40
40
41
41
#[ allow( clippy:: len_without_is_empty) ]
42
42
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.
44
46
pub fn new ( dur : Duration ) -> Self {
45
47
Self {
46
- dur : location
47
- . try_into ( )
48
- . expect ( "could not convert into a valid URL" ) ,
48
+ inner : RetryDirective :: Duration ( dur) ,
49
49
}
50
50
}
51
51
52
- /// Create a new instance from headers .
52
+ /// Create a new instance from a `SystemTime` instant .
53
53
///
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 ( ) ,
65
65
None => return Ok ( None ) ,
66
66
} ;
67
67
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) ) ,
74
70
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)
80
73
}
81
74
} ;
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)
83
90
}
84
91
85
92
/// Sets the header.
@@ -94,33 +101,53 @@ impl RetryAfter {
94
101
95
102
/// Get the `HeaderValue`.
96
103
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
+ } ;
98
108
// SAFETY: the internal string is validated to be ASCII.
99
109
unsafe { HeaderValue :: from_bytes_unchecked ( output. into ( ) ) }
100
110
}
101
111
}
102
112
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
+
103
122
#[ cfg( test) ]
104
123
mod test {
105
124
use super :: * ;
106
125
use crate :: headers:: Headers ;
107
126
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
-
111
127
#[ test]
112
128
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 ) ) ;
114
131
115
132
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 ) ) ;
124
141
Ok ( ( ) )
125
142
}
126
143
}
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