1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![cfg_attr(docsrs, allow(unused_attributes))]
3#![forbid(unsafe_code)]
4#![deny(missing_docs)]
5mod backend;
42mod error;
43mod metrics;
44mod quota;
45
46#[cfg(feature = "redis")]
47mod redis;
48
49#[cfg(feature = "sqlite")]
50mod sqlite;
51
52#[cfg(feature = "sliding-window")]
53mod sliding_window;
54
55#[cfg(feature = "tower")]
56#[cfg_attr(docsrs, doc(cfg(feature = "tower")))]
57pub mod client_ip;
58
59#[cfg(feature = "tower")]
60mod tower_layer;
61
62#[cfg(feature = "tower")]
63#[cfg_attr(docsrs, doc(cfg(feature = "tower")))]
64pub use client_ip::{
65 ClientIpConfig, ClientIpError, ClientIpSource, IpNet, MissingClientIdentity,
66 MissingClientPolicy, ResolvedClient,
67};
68#[cfg(feature = "tower")]
69#[cfg_attr(docsrs, doc(cfg(feature = "tower")))]
70pub use tower_layer::{KeyExtractor, RateLimitLayer, RateLimitService};
71
72#[cfg(feature = "in-memory")]
73#[cfg_attr(docsrs, doc(cfg(feature = "in-memory")))]
74pub use backend::InMemoryBackend;
75#[cfg(feature = "test-util")]
76#[cfg_attr(docsrs, doc(cfg(feature = "test-util")))]
77pub use backend::NoopBackend;
78pub use backend::{RateLimitBackend, gcra_decide};
79
80pub use error::RateLimitError;
81pub use metrics::RateLimitResult;
82pub use quota::Quota;
83#[cfg(feature = "sliding-window")]
84#[cfg_attr(docsrs, doc(cfg(feature = "sliding-window")))]
85pub use sliding_window::SlidingWindowBackend;
86
87#[cfg(feature = "redis")]
88#[cfg_attr(docsrs, doc(cfg(feature = "redis")))]
89pub use redis::RedisBackend;
90
91#[cfg(feature = "sqlite")]
92#[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))]
93pub use sqlite::SqliteBackend;
94
95use std::sync::Arc;
96
97pub struct RateLimiter<B: RateLimitBackend> {
99 quota: Quota,
100 backend: Arc<B>,
101}
102
103impl<B: RateLimitBackend> Clone for RateLimiter<B> {
104 fn clone(&self) -> Self {
105 Self {
106 quota: self.quota,
109 backend: Arc::clone(&self.backend),
110 }
111 }
112}
113
114impl<B: RateLimitBackend> RateLimiter<B> {
115 pub fn new(quota: Quota, backend: B) -> Self {
117 Self {
118 quota,
119 backend: Arc::new(backend),
120 }
121 }
122
123 pub async fn check(&self, key: &str) -> RateLimitResult {
125 self.backend.check(key, &self.quota).await
126 }
127
128 pub fn check_sync(&self, key: &str) -> RateLimitResult {
133 #[allow(clippy::expect_used)]
137 let rt = tokio::runtime::Builder::new_current_thread()
138 .build()
139 .expect("failed to create tokio runtime for sync check");
140 rt.block_on(self.check(key))
141 }
142}
143
144#[cfg(feature = "in-memory")]
145pub use keyed::KeyedRateLimiter;
146
147#[cfg(feature = "in-memory")]
148mod keyed {
149 use std::sync::Arc;
150
151 use dashmap::DashMap;
152
153 use crate::backend::RateLimitBackend;
154 use crate::metrics::RateLimitResult;
155 use crate::quota::Quota;
156
157 pub struct KeyedRateLimiter<B: RateLimitBackend> {
183 overrides: DashMap<String, Quota>,
186 default_quota: Quota,
187 backend: Arc<B>,
190 }
191
192 impl<B: RateLimitBackend> KeyedRateLimiter<B> {
193 pub fn new(default_quota: Quota, backend: B) -> Self {
195 Self {
196 overrides: DashMap::new(),
197 default_quota,
198 backend: Arc::new(backend),
199 }
200 }
201
202 pub fn with_quota_for_key(&self, key: &str, quota: Quota) {
207 self.overrides.insert(key.to_owned(), quota);
208 }
209
210 pub async fn check(&self, key: &str) -> RateLimitResult {
218 let quota = match self.overrides.get(key) {
219 Some(entry) => *entry.value(),
220 None => self.default_quota,
221 };
222 self.backend.check(key, "a).await
223 }
224
225 pub fn check_sync(&self, key: &str) -> RateLimitResult {
230 #[allow(clippy::expect_used)]
234 let rt = tokio::runtime::Builder::new_current_thread()
235 .build()
236 .expect("failed to create tokio runtime for sync check");
237 rt.block_on(self.check(key))
238 }
239 }
240
241 #[cfg(test)]
242 mod keyed_tests {
243 use super::KeyedRateLimiter;
244
245 use crate::backend::InMemoryBackend;
246 use crate::quota::Quota;
247
248 #[tokio::test]
249 async fn warm_path_inserts_nothing() {
250 let limiter = KeyedRateLimiter::new(Quota::per_second(10), InMemoryBackend::new());
255
256 limiter.check("warm").await;
257 limiter.check("warm").await;
258 assert!(
259 limiter.overrides.is_empty(),
260 "default-quota checks must not populate the override map"
261 );
262
263 limiter.with_quota_for_key("warm", Quota::per_second(2));
264 assert_eq!(limiter.overrides.len(), 1);
265 let r = limiter.check("warm").await;
266 assert!(r.allowed);
267 assert_eq!(r.limit, 2, "the override quota must apply");
268 }
269 }
270}
271
272#[allow(
276 clippy::unwrap_used,
277 clippy::expect_used,
278 clippy::indexing_slicing,
279 clippy::panic
280)]
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use std::time::Duration;
285
286 #[test]
287 fn quota_per_second() {
288 let q = Quota::per_second(10);
289 assert_eq!(q.burst, 10);
290 assert_eq!(q.interval(), Duration::from_millis(100));
291 }
292
293 #[test]
294 fn quota_per_minute() {
295 let q = Quota::per_minute(100);
296 assert_eq!(q.burst, 100);
297 assert_eq!(q.interval(), Duration::from_millis(600));
298 }
299
300 #[test]
301 fn quota_per_hour() {
302 let q = Quota::per_hour(3600);
303 assert_eq!(q.burst, 3600);
304 assert_eq!(q.interval(), Duration::from_secs(1));
305 }
306
307 #[test]
308 fn quota_allow_burst() {
309 let q = Quota::per_second(10).allow_burst(20);
310 assert_eq!(q.burst, 20);
311 }
312
313 #[test]
314 fn rate_limit_result_fields() {
315 let r = RateLimitResult {
316 allowed: true,
317 remaining: 5,
318 reset_at: std::time::Instant::now(),
319 limit: 10,
320 retry_after: None,
321 };
322 assert!(r.allowed);
323 assert_eq!(r.remaining, 5);
324 assert_eq!(r.limit, 10);
325 assert!(r.retry_after.is_none());
326 }
327
328 #[test]
329 fn rate_limit_result_headers() {
330 let r = RateLimitResult {
331 allowed: true,
332 remaining: 7,
333 reset_at: std::time::Instant::now() + Duration::from_secs(30),
334 limit: 10,
335 retry_after: None,
336 };
337 let headers = r.headers();
338 assert_eq!(headers.len(), 3);
339 assert_eq!(headers[0].0, "X-RateLimit-Limit");
340 assert_eq!(headers[0].1, "10");
341 assert_eq!(headers[1].0, "X-RateLimit-Remaining");
342 assert_eq!(headers[1].1, "7");
343 assert_eq!(headers[2].0, "X-RateLimit-Reset");
344 }
345
346 #[test]
347 fn rate_limit_result_retry_after() {
348 let r = RateLimitResult {
349 allowed: false,
350 remaining: 0,
351 reset_at: std::time::Instant::now(),
352 limit: 10,
353 retry_after: Some(Duration::from_millis(100)),
354 };
355 assert!(!r.allowed);
356 assert_eq!(r.retry_after, Some(Duration::from_millis(100)));
357 }
358
359 #[cfg(feature = "in-memory")]
360 #[test]
361 fn in_memory_backend_new() {
362 let _ = InMemoryBackend::new();
363 }
364
365 #[cfg(feature = "in-memory")]
366 #[test]
367 fn in_memory_backend_default() {
368 let _ = InMemoryBackend::default();
369 }
370
371 #[test]
372 fn rate_limit_error_display() {
373 let e = RateLimitError::RateLimited;
374 assert_eq!(e.to_string(), "rate limit exceeded");
375
376 let e = RateLimitError::BackendError("connection refused".into());
377 assert_eq!(
378 e.to_string(),
379 "rate limit backend error: connection refused"
380 );
381 }
382
383 #[cfg(feature = "in-memory")]
384 #[tokio::test]
385 async fn first_request_is_allowed() {
386 let backend = InMemoryBackend::new();
387 let limiter = RateLimiter::new(Quota::per_second(10), backend);
388 let result = limiter.check("user-1").await;
389 assert!(result.allowed);
390 assert_eq!(result.remaining, 1);
396 assert_eq!(result.limit, 10);
397 assert!(result.retry_after.is_none());
398 }
399
400 #[cfg(feature = "in-memory")]
401 #[tokio::test]
402 async fn rate_limit_exhaustion() {
403 let backend = InMemoryBackend::new();
404 let limiter = RateLimiter::new(Quota::per_second(2), backend);
405 let r1 = limiter.check("user-1").await;
406 assert!(r1.allowed);
407 let r2 = limiter.check("user-1").await;
408 assert!(r2.allowed);
409 let r3 = limiter.check("user-1").await;
410 assert!(!r3.allowed);
411 assert!(r3.retry_after.is_some());
412 }
413
414 #[cfg(feature = "in-memory")]
415 #[tokio::test]
416 async fn keyed_limiter_default_quota() {
417 use crate::KeyedRateLimiter;
418 let backend = InMemoryBackend::new();
419 let limiter = KeyedRateLimiter::new(Quota::per_second(5), backend);
420
421 let r = limiter.check("client-1").await;
422 assert!(r.allowed);
423 assert_eq!(r.limit, 5);
424 }
425
426 #[cfg(feature = "in-memory")]
427 #[tokio::test]
428 async fn keyed_limiter_custom_quota_per_key() {
429 use crate::KeyedRateLimiter;
430 let backend = InMemoryBackend::new();
431 let limiter = KeyedRateLimiter::new(Quota::per_second(100), backend);
432
433 limiter.with_quota_for_key("login", Quota::per_second(2));
435
436 let r_login = limiter.check("login").await;
437 assert!(r_login.allowed);
438 assert_eq!(r_login.limit, 2);
439
440 let r_api = limiter.check("api").await;
441 assert!(r_api.allowed);
442 assert_eq!(r_api.limit, 100);
443 }
444
445 #[cfg(feature = "in-memory")]
446 #[tokio::test]
447 async fn keyed_limiter_isolation() {
448 use crate::KeyedRateLimiter;
449 let backend = InMemoryBackend::new();
450 let limiter = KeyedRateLimiter::new(Quota::per_second(1), backend);
451
452 let r1 = limiter.check("a").await;
453 assert!(r1.allowed);
454
455 let r1_again = limiter.check("a").await;
457 assert!(!r1_again.allowed);
458
459 let r2 = limiter.check("b").await;
460 assert!(r2.allowed);
461 }
462
463 #[cfg(feature = "in-memory")]
464 #[test]
465 fn keyed_limiter_sync_check() {
466 use crate::KeyedRateLimiter;
467 let backend = InMemoryBackend::new();
468 let limiter = KeyedRateLimiter::new(Quota::per_second(10), backend);
469
470 let r = limiter.check_sync("client-1");
471 assert!(r.allowed);
472 assert_eq!(r.limit, 10);
473 }
474
475 #[cfg(feature = "in-memory")]
476 #[tokio::test]
477 async fn keyed_limiter_headers() {
478 use crate::KeyedRateLimiter;
479 let backend = InMemoryBackend::new();
480 let limiter = KeyedRateLimiter::new(Quota::per_second(10), backend);
481
482 let r = limiter.check("client-1").await;
483 let headers = r.headers();
484 assert!(headers.iter().any(|(k, _)| *k == "X-RateLimit-Limit"));
485 assert!(
486 headers
487 .iter()
488 .any(|(k, v)| *k == "X-RateLimit-Remaining" && v == "1")
489 );
490 }
491
492 #[cfg(feature = "in-memory")]
493 #[test]
494 fn sync_check_basic() {
495 let backend = InMemoryBackend::new();
496 let limiter = RateLimiter::new(Quota::per_second(10), backend);
497 let r = limiter.check_sync("user-1");
498 assert!(r.allowed);
499 }
500}