rate_limits/
lib.rs

1//! [![docs.rs](https://docs.rs/rate-limits/badge.svg)](https://docs.rs/rate-limits)
2//!
3//! A crate for parsing HTTP rate limit headers as per the [IETF draft][draft].
4//! Inofficial implementations like the [Github rate limit headers][github] are
5//! also supported on a best effort basis. See [vendor list] for support.
6//!
7//! ```rust
8//! use indoc::indoc;
9//! use std::str::FromStr;
10//! use time::{OffsetDateTime, Duration};
11//! use rate_limits::{Vendor, RateLimit, ResetTime};
12//!
13//! let headers = indoc! {"
14//!     x-ratelimit-limit: 5000
15//!     x-ratelimit-remaining: 4987
16//!     x-ratelimit-reset: 1350085394
17//! "};
18//!
19//! assert_eq!(
20//!     RateLimit::from_str(headers).unwrap(),
21//!     RateLimit {
22//!         limit: 5000,
23//!         remaining: 4987,
24//!         reset: ResetTime::DateTime(
25//!             OffsetDateTime::from_unix_timestamp(1350085394).unwrap()
26//!         ),
27//!         window: Some(Duration::HOUR),
28//!         vendor: Vendor::Github
29//!     },
30//! );
31//! ```
32//!
33//! Also takes the `Retry-After` header into account when calculating the reset
34//! time.
35//!
36//! [`http::HeaderMap`][headermap] is supported as well:
37//!
38//! ```rust
39//! use std::str::FromStr;
40//! use time::{OffsetDateTime, Duration};
41//! use rate_limits::{Vendor, RateLimit, ResetTime};
42//! use http::header::HeaderMap;
43//!
44//! let mut headers = HeaderMap::new();
45//! headers.insert("X-RATELIMIT-LIMIT", "5000".parse().unwrap());
46//! headers.insert("X-RATELIMIT-REMAINING", "4987".parse().unwrap());
47//! headers.insert("X-RATELIMIT-RESET", "1350085394".parse().unwrap());
48//!
49//! assert_eq!(
50//!     RateLimit::new(headers).unwrap(),
51//!     RateLimit {
52//!         limit: 5000,
53//!         remaining: 4987,
54//!         reset: ResetTime::DateTime(
55//!             OffsetDateTime::from_unix_timestamp(1350085394).unwrap()
56//!         ),
57//!         window: Some(Duration::HOUR),
58//!         vendor: Vendor::Github
59//!     },
60//! );
61//! ```
62//!
63//! ## Other resources:
64//!
65//! * [Examples of HTTP API Rate Limiting HTTP Response][stackoverflow]
66//!
67//!
68//! [draft]: https://tools.ietf.org/id/draft-polli-ratelimit-headers-00.html
69//! [headers]: https://stackoverflow.com/a/16022625/270334
70//! [github]: https://docs.github.com/en/rest/overview/resources-in-the-rest-api
71//! [vendor list]: https://docs.rs/rate-limits/latest/rate_limits/enum.Vendor.html
72//! [stackoverflow]: https://stackoverflow.com/questions/16022624/examples-of-http-api-rate-limiting-http-response-headers
73//! [headermap]: https://docs.rs/http/latest/http/header/struct.HeaderMap.html
74#![warn(clippy::all)]
75#![warn(
76    absolute_paths_not_starting_with_crate,
77    rustdoc::invalid_html_tags,
78    missing_copy_implementations,
79    missing_debug_implementations,
80    semicolon_in_expressions_from_macros,
81    unreachable_pub,
82    unused_crate_dependencies,
83    unused_extern_crates,
84    variant_size_differences,
85    clippy::missing_const_for_fn
86)]
87#![deny(anonymous_parameters, macro_use_extern_crate, pointer_structural_match)]
88#![deny(missing_docs)]
89#![allow(clippy::module_name_repetitions)]
90
91mod convert;
92mod error;
93mod types;
94mod variants;
95
96use std::str::FromStr;
97
98use error::{Error, Result};
99use headers::HeaderValue;
100use types::CaseSensitiveHeaderMap;
101use variants::RATE_LIMIT_HEADERS;
102
103use time::{format_description::well_known::Rfc2822, Date, Duration};
104use types::Used;
105pub use types::{Limit, RateLimitVariant, Remaining, ResetTime, ResetTimeKind, Vendor};
106
107/// HTTP rate limits as parsed from header values
108#[derive(Copy, Clone, Debug, PartialEq)]
109pub struct RateLimit {
110    /// The maximum number of requests allowed in the time window
111    pub limit: usize,
112    /// The number of requests remaining in the time window
113    pub remaining: usize,
114    /// The time at which the rate limit will be reset
115    pub reset: ResetTime,
116    /// The time window until the rate limit is lifted.
117    /// It is optional, because it might not be given,
118    /// in which case it needs to be inferred from the environment
119    pub window: Option<Duration>,
120    /// Predicted vendor based on rate limit header
121    pub vendor: Vendor,
122}
123
124impl RateLimit {
125    /// Extracts rate limits from HTTP headers separated by newlines
126    ///
127    /// There are different header names for various websites
128    /// Github, Vimeo, Twitter, Imgur, etc have their own headers.
129    /// Without additional context, the parsing is done on a best-effort basis.
130    pub fn new<T: Into<CaseSensitiveHeaderMap>>(headers: T) -> std::result::Result<Self, Error> {
131        let headers = headers.into();
132        let value = Self::get_remaining_header(&headers)?;
133        let remaining = Remaining::new(value.to_str()?)?;
134
135        let (limit, variant) = if let Ok((limit, variant)) = Self::get_rate_limit_header(&headers) {
136            (Limit::new(limit.to_str()?)?, variant)
137        } else if let Ok((used, variant)) = Self::get_used_header(&headers) {
138            // The site provides a `used` header, but no `limit` header.
139            // Therefore we have to calculate the limit from used and remaining.
140            let used = Used::new(used.to_str()?)?;
141            let limit = used.count + remaining.count;
142            (Limit::from(limit), variant)
143        } else {
144            return Err(Error::MissingUsed);
145        };
146
147        // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
148        let reset = if let Some(date_or_seconds) = Self::get_retry_after_header(&headers) {
149            if Date::parse(date_or_seconds.to_str()?, &Rfc2822).is_ok() {
150                ResetTime::new(date_or_seconds, ResetTimeKind::ImfFixdate)?
151            } else {
152                ResetTime::new(date_or_seconds, ResetTimeKind::Seconds)?
153            }
154        } else {
155            let (value, kind) = Self::get_reset_header(&headers)?;
156            ResetTime::new(value, kind)?
157        };
158
159        Ok(RateLimit {
160            limit: limit.count,
161            remaining: remaining.count,
162            reset,
163            window: variant.duration,
164            vendor: variant.vendor,
165        })
166    }
167
168    fn get_rate_limit_header(
169        header_map: &CaseSensitiveHeaderMap,
170    ) -> Result<(&HeaderValue, RateLimitVariant)> {
171        let variants = RATE_LIMIT_HEADERS.lock().map_err(|_| Error::Lock)?;
172
173        for variant in variants.iter() {
174            if let Some(limit) = &variant.limit_header {
175                if let Some(value) = header_map.get(limit) {
176                    return Ok((value, variant.clone()));
177                }
178            }
179        }
180        Err(Error::MissingLimit)
181    }
182
183    fn get_used_header(
184        header_map: &CaseSensitiveHeaderMap,
185    ) -> Result<(&HeaderValue, RateLimitVariant)> {
186        let variants = RATE_LIMIT_HEADERS.lock().map_err(|_| Error::Lock)?;
187
188        for variant in variants.iter() {
189            if let Some(used) = &variant.used_header {
190                if let Some(value) = header_map.get(used) {
191                    return Ok((value, variant.clone()));
192                }
193            }
194        }
195        Err(Error::MissingUsed)
196    }
197
198    fn get_remaining_header(header_map: &CaseSensitiveHeaderMap) -> Result<&HeaderValue> {
199        let variants = RATE_LIMIT_HEADERS.lock().map_err(|_| Error::Lock)?;
200
201        for variant in variants.iter() {
202            if let Some(value) = header_map.get(&variant.remaining_header) {
203                return Ok(value);
204            }
205        }
206        Err(Error::MissingRemaining)
207    }
208
209    fn get_reset_header(
210        header_map: &CaseSensitiveHeaderMap,
211    ) -> Result<(&HeaderValue, ResetTimeKind)> {
212        let variants = RATE_LIMIT_HEADERS.lock().map_err(|_| Error::Lock)?;
213
214        for variant in variants.iter() {
215            if let Some(value) = header_map.get(&variant.reset_header) {
216                return Ok((value, variant.reset_kind));
217            }
218        }
219        Err(Error::MissingRemaining)
220    }
221
222    fn get_retry_after_header(header_map: &CaseSensitiveHeaderMap) -> Option<&HeaderValue> {
223        header_map.get("Retry-After")
224    }
225
226    /// Get the number of requests allowed in the time window
227    #[must_use]
228    pub const fn limit(&self) -> usize {
229        self.limit
230    }
231
232    /// Get the number of requests remaining in the time window
233    #[must_use]
234    pub const fn remaining(&self) -> usize {
235        self.remaining
236    }
237
238    /// Get the time at which the rate limit will be reset
239    #[must_use]
240    pub const fn reset(&self) -> ResetTime {
241        self.reset
242    }
243}
244
245impl FromStr for RateLimit {
246    type Err = Error;
247
248    fn from_str(map: &str) -> Result<Self> {
249        RateLimit::new(CaseSensitiveHeaderMap::from_str(map)?)
250    }
251}
252
253#[cfg(test)]
254mod tests {
255
256    use crate::types::HeaderMapExt;
257
258    use super::*;
259    use headers::HeaderMap;
260    use indoc::indoc;
261    use time::{macros::datetime, OffsetDateTime};
262
263    #[test]
264    fn parse_limit_value() {
265        let limit = Limit::new("  23 ").unwrap();
266        assert_eq!(limit.count, 23);
267    }
268
269    #[test]
270    fn parse_invalid_limit_value() {
271        assert!(Limit::new("foo").is_err());
272        assert!(Limit::new("0 foo").is_err());
273        assert!(Limit::new("bar 0").is_err());
274    }
275
276    #[test]
277    fn parse_vendor() {
278        let map = CaseSensitiveHeaderMap::from_str("x-ratelimit-limit: 5000").unwrap();
279        let (_, variant) = RateLimit::get_rate_limit_header(&map).unwrap();
280        assert_eq!(variant.vendor, Vendor::Github);
281
282        let map = CaseSensitiveHeaderMap::from_str("RateLimit-Limit: 5000").unwrap();
283        let (_, variant) = RateLimit::get_rate_limit_header(&map).unwrap();
284        assert_eq!(variant.vendor, Vendor::Standard);
285    }
286
287    #[test]
288    fn parse_retry_after_seconds() {
289        let map = CaseSensitiveHeaderMap::from_str("Retry-After: 30").unwrap();
290        let retry = RateLimit::get_retry_after_header(&map).unwrap();
291
292        assert_eq!("30", retry);
293    }
294
295    #[test]
296    fn parse_remaining_value() {
297        let remaining = Remaining::new("  23 ").unwrap();
298        assert_eq!(remaining.count, 23);
299    }
300
301    #[test]
302    fn parse_invalid_remaining_value() {
303        assert!(Remaining::new("foo").is_err());
304        assert!(Remaining::new("0 foo").is_err());
305        assert!(Remaining::new("bar 0").is_err());
306    }
307
308    #[test]
309    fn parse_reset_timestamp() {
310        let v = HeaderValue::from_str("1350085394").unwrap();
311        assert_eq!(
312            ResetTime::new(&v, ResetTimeKind::Timestamp).unwrap(),
313            ResetTime::DateTime(OffsetDateTime::from_unix_timestamp(1_350_085_394).unwrap())
314        );
315    }
316
317    #[test]
318    fn parse_reset_seconds() {
319        let v = HeaderValue::from_str("100").unwrap();
320        assert_eq!(
321            ResetTime::new(&v, ResetTimeKind::Seconds).unwrap(),
322            ResetTime::Seconds(100)
323        );
324    }
325
326    #[test]
327    fn parse_reset_datetime() {
328        let v = HeaderValue::from_str("Tue, 15 Nov 1994 08:12:31 GMT").unwrap();
329        let d = ResetTime::new(&v, ResetTimeKind::ImfFixdate);
330        assert_eq!(
331            d.unwrap(),
332            ResetTime::DateTime(datetime!(1994-11-15 8:12:31 UTC))
333        );
334    }
335
336    #[test]
337    fn parse_header_map_newlines() {
338        let map = HeaderMap::from_raw(
339            "x-ratelimit-limit: 5000
340x-ratelimit-remaining: 4987
341x-ratelimit-reset: 1350085394
342",
343        )
344        .unwrap();
345
346        assert_eq!(map.len(), 3);
347        assert_eq!(
348            map.get("x-ratelimit-limit"),
349            Some(&HeaderValue::from_str("5000").unwrap())
350        );
351        assert_eq!(
352            map.get("x-ratelimit-remaining"),
353            Some(&HeaderValue::from_str("4987").unwrap())
354        );
355        assert_eq!(
356            map.get("x-ratelimit-reset"),
357            Some(&HeaderValue::from_str("1350085394").unwrap())
358        );
359    }
360
361    #[test]
362    fn parse_github_headers() {
363        let headers = indoc! {"
364            x-ratelimit-limit: 5000
365            x-ratelimit-remaining: 4987
366            x-ratelimit-reset: 1350085394
367        "};
368
369        let rate = RateLimit::from_str(headers).unwrap();
370        assert_eq!(rate.limit(), 5000);
371        assert_eq!(rate.remaining(), 4987);
372        assert_eq!(
373            rate.reset(),
374            ResetTime::DateTime(OffsetDateTime::from_unix_timestamp(1_350_085_394).unwrap())
375        );
376    }
377
378    #[test]
379    fn parse_reddit_headers() {
380        let headers = indoc! {"
381            X-Ratelimit-Used: 100
382            X-Ratelimit-Remaining: 22
383            X-Ratelimit-Reset: 30
384        "};
385
386        let rate = RateLimit::from_str(headers).unwrap();
387        assert_eq!(rate.limit(), 122);
388        assert_eq!(rate.remaining(), 22);
389        assert_eq!(rate.reset(), ResetTime::Seconds(30));
390    }
391
392    #[test]
393    fn parse_gitlab_headers() {
394        let headers = indoc! {"
395            RateLimit-Limit: 60
396            RateLimit-Observed: 67
397            RateLimit-Remaining: 0
398            RateLimit-Reset: 1609844400 
399        "};
400
401        let rate = RateLimit::from_str(headers).unwrap();
402        assert_eq!(rate.limit(), 60);
403        assert_eq!(rate.remaining(), 0);
404        assert_eq!(
405            rate.reset(),
406            ResetTime::DateTime(OffsetDateTime::from_unix_timestamp(1_609_844_400).unwrap())
407        );
408    }
409
410    #[test]
411    fn retry_after_seconds_takes_precedence_over_reset() {
412        let headers = indoc! {"
413            X-Ratelimit-Used: 100
414            X-Ratelimit-Remaining: 22
415            X-Ratelimit-Reset: 30
416            Retry-After: 20
417        "};
418
419        let rate = RateLimit::from_str(headers).unwrap();
420        assert_eq!(rate.limit(), 122);
421        assert_eq!(rate.remaining(), 22);
422        assert_eq!(rate.reset(), ResetTime::Seconds(20));
423    }
424
425    #[test]
426    fn retry_after_date_takes_precedence_over_reset() {
427        let headers = indoc! {"
428            X-Ratelimit-Used: 100
429            X-Ratelimit-Remaining: 22
430            X-Ratelimit-Reset: 30
431            Retry-After: Wed, 21 Oct 2015 07:28:00 GMT
432        "};
433
434        let rate = RateLimit::from_str(headers).unwrap();
435        assert_eq!(rate.limit(), 122);
436        assert_eq!(rate.remaining(), 22);
437        assert_eq!(
438            rate.reset(),
439            ResetTime::DateTime(datetime!(2015-10-21 7:28:00.0 UTC))
440        );
441    }
442}