Skip to main content

rate_limits/
lib.rs

1#![doc = include_str!("../README.md")]
2
3mod convert;
4mod error;
5mod parser;
6mod reset_time;
7mod vendors;
8
9pub mod headers;
10pub mod retry_after;
11
12use std::str::FromStr;
13
14use error::{Error, Result};
15#[cfg(feature = "http")]
16use http::HeaderMap;
17
18use std::time::Duration;
19
20pub use headers::Headers;
21pub use reset_time::ResetTime;
22pub use vendors::{Vendor, VendorMask};
23
24/// The status of the rate limit.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Status {
27    /// The rate limit has not been reached.
28    /// You can make requests immediately.
29    Ready,
30    /// The rate limit has been reached.
31    /// The associated duration is the time to wait until the limit resets.
32    Wait(Duration),
33}
34
35/// Rate Limit information, parsed from HTTP headers.
36///
37/// There are multiple ways to represent rate limit information in HTTP headers.
38/// The following variants are supported:
39///
40/// - [IETF "Polly" draft][ietf]
41/// - [Retry-After][retryafter]
42///
43/// [ietf]: https://datatracker.ietf.org/doc/html/draft-polli-ratelimit-headers-00
44/// [retryafter]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After
45///
46#[derive(Copy, Clone, Debug, PartialEq)]
47pub enum RateLimit {
48    /// Rate limit information as per the [IETF "Polly" draft][ietf].
49    Rfc6585(headers::Headers),
50    /// Rate limit information as per the [Retry-After][retryafter] header.
51    RetryAfter(retry_after::RateLimit),
52}
53
54impl RateLimit {
55    /// Create a new `RateLimit` from a `http::HeaderMap`.
56    #[cfg(feature = "http")]
57    pub fn new(headers: &HeaderMap) -> std::result::Result<Self, Error> {
58        Self::extract(crate::convert::header_map_str_pairs(headers))
59    }
60
61    /// Create a new `RateLimit` from an iterator over HTTP headers.
62    pub fn extract<'a, I>(headers: I) -> std::result::Result<Self, Error>
63    where
64        I: IntoIterator<Item = (&'a str, &'a str)> + Clone,
65    {
66        let rfc6585 = headers::Headers::extract(headers.clone());
67        let retryafter = retry_after::RateLimit::extract(headers);
68
69        match (rfc6585, retryafter) {
70            (Ok(rfc6585), Ok(retryafter)) => {
71                // If both are present, we pick the one that requires us to wait longer.
72                // This is a pessimistic approach to ensure we don't hit the rate limit.
73                if rfc6585.reset.duration() > retryafter.reset.duration() {
74                    Ok(Self::Rfc6585(rfc6585))
75                } else {
76                    Ok(Self::RetryAfter(retryafter))
77                }
78            }
79            (Ok(rfc6585), Err(_)) => Ok(Self::Rfc6585(rfc6585)),
80            (Err(_), Ok(retryafter)) => Ok(Self::RetryAfter(retryafter)),
81            (Err(e), Err(_)) => Err(e),
82        }
83    }
84
85    /// Check if the rate limit has been reached.
86    pub const fn is_limited(&self) -> bool {
87        match self {
88            Self::Rfc6585(headers) => headers.remaining == 0,
89            Self::RetryAfter(_) => true,
90        }
91    }
92
93    /// Get the current status of the rate limit.
94    pub fn status(&self) -> Status {
95        if self.is_limited() {
96            Status::Wait(self.reset().duration())
97        } else {
98            Status::Ready
99        }
100    }
101
102    /// Get `reset` time.
103    /// This is the time when the rate limit will be reset.
104    pub const fn reset(&self) -> ResetTime {
105        match self {
106            Self::Rfc6585(rfc6585) => rfc6585.reset,
107            Self::RetryAfter(retryafter) => retryafter.reset,
108        }
109    }
110
111    /// Get `limit` value.
112    ///
113    /// This is the maximum number of requests that can be made in a given time window.
114    pub const fn limit(&self) -> Option<usize> {
115        match self {
116            Self::Rfc6585(rfc6585) => Some(rfc6585.limit),
117            Self::RetryAfter(_) => None,
118        }
119    }
120
121    /// Get `remaining` value.
122    ///
123    /// This is the number of requests remaining in the current time window.
124    pub const fn remaining(&self) -> Option<usize> {
125        match self {
126            Self::Rfc6585(rfc6585) => Some(rfc6585.remaining),
127            Self::RetryAfter(_) => None,
128        }
129    }
130}
131
132impl FromStr for RateLimit {
133    type Err = Error;
134
135    fn from_str(map: &str) -> Result<Self> {
136        RateLimit::extract(crate::convert::parse_header_lines(map))
137    }
138}
139
140#[cfg(feature = "reqwest")]
141impl TryFrom<&reqwest::Response> for RateLimit {
142    type Error = Error;
143
144    fn try_from(response: &reqwest::Response) -> Result<Self> {
145        Self::extract(crate::convert::header_map_str_pairs(response.headers()))
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use indoc::indoc;
153    use std::str::FromStr;
154    use time::macros::datetime;
155
156    use crate::reset_time::ResetTime;
157
158    #[test]
159    fn use_later_reset_time_date() {
160        let headers = indoc! {"
161            X-Ratelimit-Used: 100
162            X-Ratelimit-Remaining: 22
163            X-Ratelimit-Reset: 30
164            Retry-After: Wed, 21 Oct 2099 07:28:00 GMT
165        "};
166
167        let rate = RateLimit::from_str(headers).unwrap();
168        assert_eq!(
169            rate.reset(),
170            ResetTime::DateTime(datetime!(2099-10-21 7:28:00.0 UTC))
171        );
172    }
173
174    #[test]
175    fn use_later_reset_time_seconds() {
176        let headers = indoc! {"
177            X-Ratelimit-Used: 100
178            X-Ratelimit-Remaining: 22
179            X-Ratelimit-Reset: 30
180            Retry-After: 20
181        "};
182
183        let rate = RateLimit::from_str(headers).unwrap();
184        assert_eq!(rate.reset(), ResetTime::Seconds(30));
185    }
186
187    #[test]
188    fn test_status_is_limited() {
189        let headers = indoc! {"
190            RateLimit-Limit: 10
191            RateLimit-Remaining: 0
192            RateLimit-Reset: 30
193        "};
194        let rate = RateLimit::from_str(headers).unwrap();
195        assert!(rate.is_limited());
196        match rate.status() {
197            Status::Wait(d) => assert_eq!(d, std::time::Duration::from_secs(30)),
198            _ => panic!("Expected Status::Wait"),
199        }
200
201        let headers = indoc! {"
202            RateLimit-Limit: 10
203            RateLimit-Remaining: 1
204            RateLimit-Reset: 30
205        "};
206        let rate = RateLimit::from_str(headers).unwrap();
207        assert!(!rate.is_limited());
208        assert_eq!(rate.status(), Status::Ready);
209    }
210}