1- use std:: time:: Duration ;
2- use std:: { convert:: TryInto , str:: FromStr } ;
1+ use std:: time:: { Duration , SystemTime , SystemTimeError } ;
32
43use 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 ) ]
3737pub struct RetryAfter {
38- dur : Duration ,
38+ inner : RetryDirective ,
3939}
4040
4141#[ allow( clippy:: len_without_is_empty) ]
4242impl 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) ]
104123mod 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