rate_limits/headers/
mod.rs1mod types;
5mod variants;
6
7use std::str::FromStr;
8
9use crate::{casesensitive_headermap::CaseSensitiveHeaderMap, reset_time::ResetTime};
10
11use super::error::{Error, Result};
12use variants::RATE_LIMIT_HEADERS;
13
14use time::Duration;
15use types::Used;
16pub use types::Vendor;
17pub(crate) use types::{Limit, RateLimitVariant, Remaining};
18
19#[derive(Copy, Clone, Debug, PartialEq)]
21pub struct Headers {
22 pub limit: usize,
24 pub remaining: usize,
26 pub reset: ResetTime,
28 pub window: Option<Duration>,
32 pub vendor: Vendor,
34}
35
36impl Headers {
37 pub fn new<T: Into<CaseSensitiveHeaderMap>>(headers: T) -> std::result::Result<Self, Error> {
57 let headers = headers.into();
58
59 let variants = Self::find_variants(&headers);
60
61 if variants.is_empty() {
62 return Err(Error::NoMatchingVariant);
63 }
64
65 let mut last_error = Error::NoMatchingVariant;
66
67 for variant in variants {
68 match Self::try_parse(&headers, &variant) {
69 Ok(headers) => return Ok(headers),
70 Err(e) => last_error = e,
71 }
72 }
73
74 Err(last_error)
75 }
76
77 fn try_parse(headers: &CaseSensitiveHeaderMap, variant: &RateLimitVariant) -> Result<Self> {
78 let value = headers
79 .get(&variant.remaining_header)
80 .ok_or(Error::MissingRemaining)?;
81 let remaining = Remaining::new(value.to_str()?)?;
82
83 let limit = if let Some(limit) = &variant.limit_header {
84 let value = headers.get(limit).ok_or(Error::MissingLimit)?;
85 Limit::new(value.to_str()?)?
86 } else if let Some(used) = &variant.used_header {
87 let value = headers.get(used).ok_or(Error::MissingUsed)?;
88 let used = Used::new(value.to_str()?)?;
89 Limit::from(used.count.saturating_add(remaining.count))
90 } else {
91 return Err(Error::MissingLimit);
92 };
93
94 let value = headers
95 .get(&variant.reset_header)
96 .ok_or(Error::MissingReset)?;
97 let reset = ResetTime::new(value, variant.reset_kind)?;
98
99 Ok(Headers {
100 limit: limit.count,
101 remaining: remaining.count,
102 reset,
103 window: variant.duration,
104 vendor: variant.vendor,
105 })
106 }
107
108 fn find_variants(headers: &CaseSensitiveHeaderMap) -> Vec<RateLimitVariant> {
109 let mut variants = Vec::new();
110
111 for variant in RATE_LIMIT_HEADERS.iter() {
112 let has_remaining = headers.get(&variant.remaining_header).is_some();
115 let has_reset = headers.get(&variant.reset_header).is_some();
116
117 let has_limit = variant
122 .limit_header
123 .as_ref()
124 .is_some_and(|h| headers.get(h).is_some());
125 let has_used = variant
126 .used_header
127 .as_ref()
128 .is_some_and(|h| headers.get(h).is_some());
129
130 if has_remaining && has_reset && (has_limit || has_used) {
135 variants.push(variant.clone());
136 }
137 }
138 variants
139 }
140
141 #[must_use]
143 pub const fn limit(&self) -> usize {
144 self.limit
145 }
146
147 #[must_use]
149 pub const fn remaining(&self) -> usize {
150 self.remaining
151 }
152
153 #[must_use]
155 pub const fn reset(&self) -> ResetTime {
156 self.reset
157 }
158}
159
160impl FromStr for Headers {
161 type Err = Error;
162
163 fn from_str(map: &str) -> Result<Self> {
164 Headers::new(CaseSensitiveHeaderMap::from_str(map)?)
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::casesensitive_headermap::HeaderMapExt;
172 use crate::reset_time::ResetTimeKind;
173 use headers::{HeaderMap, HeaderValue};
174 use indoc::indoc;
175 use time::{OffsetDateTime, macros::datetime};
176
177 #[test]
178 fn parse_limit_value() {
179 let limit = Limit::new(" 23 ").unwrap();
180 assert_eq!(limit.count, 23);
181 }
182
183 #[test]
184 fn parse_invalid_limit_value() {
185 assert!(Limit::new("foo").is_err());
186 assert!(Limit::new("0 foo").is_err());
187 assert!(Limit::new("bar 0").is_err());
188 }
189
190 #[test]
191 fn parse_vendor() {
192 let map = CaseSensitiveHeaderMap::from_str(
193 "x-ratelimit-limit: 5000\nx-ratelimit-remaining: 5\nx-ratelimit-reset: 1350085394",
194 )
195 .unwrap();
196 let variants = Headers::find_variants(&map);
197 assert_eq!(variants[0].vendor, Vendor::Github);
198
199 let map = CaseSensitiveHeaderMap::from_str(
200 "RateLimit-Limit: 5000\nRatelimit-Remaining: 5\nRatelimit-Reset: 10",
201 )
202 .unwrap();
203 let variants = Headers::find_variants(&map);
204 assert_eq!(variants[0].vendor, Vendor::PolliDraft);
205 }
206
207 #[test]
208 fn parse_remaining_value() {
209 let remaining = Remaining::new(" 23 ").unwrap();
210 assert_eq!(remaining.count, 23);
211 }
212
213 #[test]
214 fn parse_invalid_remaining_value() {
215 assert!(Remaining::new("foo").is_err());
216 assert!(Remaining::new("0 foo").is_err());
217 assert!(Remaining::new("bar 0").is_err());
218 }
219
220 #[test]
221 fn parse_reset_timestamp() {
222 let v = HeaderValue::from_str("1350085394").unwrap();
223 assert_eq!(
224 ResetTime::new(&v, ResetTimeKind::Timestamp).unwrap(),
225 ResetTime::DateTime(OffsetDateTime::from_unix_timestamp(1_350_085_394).unwrap())
226 );
227 }
228
229 #[test]
230 fn parse_reset_seconds() {
231 let v = HeaderValue::from_str("100").unwrap();
232 assert_eq!(
233 ResetTime::new(&v, ResetTimeKind::Seconds).unwrap(),
234 ResetTime::Seconds(100)
235 );
236 }
237
238 #[test]
239 fn parse_reset_datetime() {
240 let v = HeaderValue::from_str("Tue, 15 Nov 1994 08:12:31 GMT").unwrap();
241 let d = ResetTime::new(&v, ResetTimeKind::ImfFixdate);
242 assert_eq!(
243 d.unwrap(),
244 ResetTime::DateTime(datetime!(1994-11-15 8:12:31 UTC))
245 );
246 }
247
248 #[test]
249 fn parse_header_map_newlines() {
250 let map = HeaderMap::from_raw(
251 "x-ratelimit-limit: 5000
252x-ratelimit-remaining: 4987
253x-ratelimit-reset: 1350085394
254",
255 )
256 .unwrap();
257
258 assert_eq!(map.len(), 3);
259 assert_eq!(
260 map.get("x-ratelimit-limit"),
261 Some(&HeaderValue::from_str("5000").unwrap())
262 );
263 assert_eq!(
264 map.get("x-ratelimit-remaining"),
265 Some(&HeaderValue::from_str("4987").unwrap())
266 );
267 assert_eq!(
268 map.get("x-ratelimit-reset"),
269 Some(&HeaderValue::from_str("1350085394").unwrap())
270 );
271 }
272
273 #[test]
274 fn parse_github_headers() {
275 let headers = indoc! {"
276 x-ratelimit-limit: 5000
277 x-ratelimit-remaining: 4987
278 x-ratelimit-reset: 1350085394
279 "};
280
281 let rate = Headers::from_str(headers).unwrap();
282 assert_eq!(rate.limit(), 5000);
283 assert_eq!(rate.remaining(), 4987);
284 assert_eq!(
285 rate.reset(),
286 ResetTime::DateTime(OffsetDateTime::from_unix_timestamp(1_350_085_394).unwrap())
287 );
288 }
289
290 #[test]
291 fn parse_reddit_headers() {
292 let headers = indoc! {"
293 X-Ratelimit-Used: 100
294 X-Ratelimit-Remaining: 22
295 X-Ratelimit-Reset: 30
296 "};
297
298 let rate = Headers::from_str(headers).unwrap();
299 assert_eq!(rate.limit(), 122);
300 assert_eq!(rate.remaining(), 22);
301 assert_eq!(rate.reset(), ResetTime::Seconds(30));
302 }
303
304 #[test]
305 fn parse_linear_headers() {
306 let headers = indoc! {"
307 X-RateLimit-Requests-Limit: 1500
308 X-RateLimit-Requests-Remaining: 1499
309 X-RateLimit-Requests-Reset: 1694721826678
310 "};
311
312 let rate = Headers::from_str(headers).unwrap();
313 assert_eq!(rate.limit(), 1500);
314 assert_eq!(rate.remaining(), 1499);
315 assert_eq!(
316 rate.reset(),
317 ResetTime::DateTime(
318 OffsetDateTime::from_unix_timestamp_nanos(1_694_721_826_678_000_000).unwrap()
321 )
322 );
323 }
324
325 #[test]
326 fn parse_gitlab_headers() {
327 let headers = indoc! {"
328 RateLimit-Limit: 60
329 RateLimit-Observed: 67
330 RateLimit-Remaining: 0
331 RateLimit-Reset: 1609844400
332 "};
333
334 let rate = Headers::from_str(headers).unwrap();
335 assert_eq!(rate.limit(), 60);
336 assert_eq!(rate.remaining(), 0);
337 assert_eq!(
338 rate.reset(),
339 ResetTime::DateTime(OffsetDateTime::from_unix_timestamp(1_609_844_400).unwrap())
340 );
341 }
342
343 #[test]
344 fn parse_twilio_headers() {
345 let headers = indoc! {"
346 X-RateLimit-Limit: 500
347 X-RateLimit-Remaining: 499
348 X-RateLimit-Reset: 1392815263
349 "};
350
351 let rate = Headers::from_str(headers).unwrap();
352 assert_eq!(rate.limit(), 500);
353 assert_eq!(rate.remaining(), 499);
354 assert_eq!(
355 rate.reset(),
356 ResetTime::DateTime(OffsetDateTime::from_unix_timestamp(1_392_815_263).unwrap())
357 );
358 }
359
360 #[test]
361 fn parse_vimeo_headers() {
362 let headers = indoc! {"
363 X-RateLimit-Limit: 500
364 X-RateLimit-Remaining: 499
365 X-RateLimit-Reset: Thu, 14 Sep 2023 21:00:00 GMT
366 "};
367
368 let rate = Headers::from_str(headers).unwrap();
369 assert_eq!(rate.limit(), 500);
370 assert_eq!(rate.remaining(), 499);
371 assert_eq!(rate.vendor, Vendor::Vimeo);
372 assert_eq!(
373 rate.reset(),
374 ResetTime::DateTime(datetime!(2023-09-14 21:00:00 UTC))
375 );
376 }
377
378 #[test]
379 fn parse_openai_headers() {
380 let headers = indoc! {"
381 x-ratelimit-limit-requests: 60
382 x-ratelimit-remaining-requests: 59
383 x-ratelimit-reset-requests: 1s
384 "};
385
386 let rate = Headers::from_str(headers).unwrap();
387 assert_eq!(rate.limit(), 60);
388 assert_eq!(rate.remaining(), 59);
389 assert_eq!(rate.vendor, Vendor::OpenAI);
390 assert_eq!(rate.reset(), ResetTime::Seconds(1));
391
392 let headers = indoc! {"
393 x-ratelimit-limit-requests: 60
394 x-ratelimit-remaining-requests: 59
395 x-ratelimit-reset-requests: 6m0s
396 "};
397
398 let rate = Headers::from_str(headers).unwrap();
399 assert_eq!(rate.reset(), ResetTime::Seconds(360));
400 }
401
402 #[test]
403 fn parse_unknown_headers() {
404 let headers = indoc! {"
405 X-Unknown-Limit: 5000
406 X-Unknown-Remaining: 4987
407 X-Unknown-Reset: 1350085394
408 "};
409
410 assert!(Headers::from_str(headers).is_err());
411 }
412
413 #[test]
414 fn parse_garbage_headers() {
415 let headers = indoc! {"
416 RateLimit-Limit: foo
417 Ratelimit-Remaining: bar
418 Ratelimit-Reset: baz
419 "};
420
421 assert!(Headers::from_str(headers).is_err());
423 }
424
425 #[test]
426 fn parse_case_sensitive_check() {
427 let headers = indoc! {"
434 RateLimit-Limit: 5000
435 RATELIMIT-REMAINING: 5
436 RATELIMIT-RESET: 10
437 "};
438
439 assert!(Headers::from_str(headers).is_err());
440 }
441}