Skip to main content

rate_limits/
retry_after.rs

1//! Retry-After header parsing
2//!
3//! See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After>
4use std::str::FromStr;
5
6use time::{Date, format_description::well_known::Rfc2822};
7
8use crate::reset_time::{ResetTime, ResetTimeKind};
9
10use crate::error::{Error, Result};
11
12/// HTTP rate limits as parsed from header values
13#[derive(Copy, Clone, Debug, PartialEq)]
14pub struct RateLimit {
15    /// Time at which the rate limit will be reset
16    pub reset: ResetTime,
17}
18
19impl RateLimit {
20    /// Rate limit implementation based on `Retry-After` header value
21    ///
22    /// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After>
23    #[cfg(feature = "http")]
24    pub fn new(headers: &http::HeaderMap) -> std::result::Result<Self, Error> {
25        Self::extract(crate::convert::header_map_str_pairs(headers))
26    }
27
28    /// Rate limit implementation based on `Retry-After` header value from an iterator
29    pub fn extract<'a, I>(headers: I) -> std::result::Result<Self, Error>
30    where
31        I: IntoIterator<Item = (&'a str, &'a str)>,
32    {
33        let mut retry_after_val = None;
34        for (k, v) in headers {
35            if k.eq_ignore_ascii_case("retry-after") {
36                retry_after_val = Some(v);
37                break;
38            }
39        }
40
41        let reset = match retry_after_val {
42            Some(retry_after_str) => {
43                if Date::parse(retry_after_str, &Rfc2822).is_ok() {
44                    ResetTime::new(retry_after_str, ResetTimeKind::ImfFixdate)?
45                } else {
46                    ResetTime::new(retry_after_str, ResetTimeKind::Seconds)?
47                }
48            }
49            None => return Err(Error::MissingRetryAfter),
50        };
51
52        Ok(RateLimit { reset })
53    }
54
55    /// Get the time at which the rate limit will be reset
56    #[must_use]
57    pub const fn reset(&self) -> ResetTime {
58        self.reset
59    }
60}
61
62impl FromStr for RateLimit {
63    type Err = Error;
64
65    fn from_str(map: &str) -> Result<Self> {
66        RateLimit::extract(crate::convert::parse_header_lines(map))
67    }
68}
69
70#[cfg(test)]
71mod tests {
72
73    use super::*;
74    use indoc::indoc;
75    use time::macros::datetime;
76
77    #[test]
78    fn retry_after_seconds() {
79        let headers = indoc! {"
80            Retry-After: 19
81        "};
82
83        let rate = RateLimit::from_str(headers).unwrap();
84        assert_eq!(rate.reset(), ResetTime::Seconds(19));
85    }
86
87    #[test]
88    fn retry_after_seconds_case_sensitive() {
89        let headers = indoc! {"
90            retry-after: 19
91        "};
92
93        let rate = RateLimit::from_str(headers).unwrap();
94        assert_eq!(rate.reset(), ResetTime::Seconds(19));
95    }
96
97    #[test]
98    fn retry_after_imf_fixdate() {
99        let headers = indoc! {"
100            Retry-After: Fri, 31 Dec 1999 23:59:59 GMT
101        "};
102
103        let rate = RateLimit::from_str(headers).unwrap();
104        assert_eq!(
105            rate.reset(),
106            ResetTime::DateTime(datetime!(1999-12-31 23:59:59 UTC))
107        );
108    }
109}