1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3mod backend;
38mod error;
39mod metrics;
40mod quota;
41
42#[cfg(feature = "redis")]
43mod redis;
44
45#[cfg(feature = "sqlite")]
46mod sqlite;
47
48#[cfg(feature = "sliding-window")]
49mod sliding_window;
50
51#[cfg(feature = "tower")]
52pub mod client_ip;
53
54#[cfg(feature = "tower")]
55mod tower_layer;
56
57#[cfg(feature = "tower")]
58pub use client_ip::{
59 ClientIpConfig, ClientIpError, ClientIpSource, IpNet, MissingClientIdentity,
60 MissingClientPolicy, ResolvedClient,
61};
62#[cfg(feature = "tower")]
63pub use tower_layer::{KeyExtractor, RateLimitLayer, RateLimitService};
64
65pub use backend::{InMemoryBackend, RateLimitBackend};
66
67pub use error::RateLimitError;
68pub use metrics::RateLimitResult;
69pub use quota::Quota;
70#[cfg(feature = "sliding-window")]
71pub use sliding_window::SlidingWindowBackend;
72
73#[cfg(feature = "redis")]
74pub use redis::RedisBackend;
75
76#[cfg(feature = "sqlite")]
77pub use sqlite::SqliteBackend;
78
79use std::sync::Arc;
80
81#[derive(Clone)]
83pub struct RateLimiter<B: RateLimitBackend> {
84 quota: Quota,
85 backend: Arc<B>,
86}
87
88impl<B: RateLimitBackend> RateLimiter<B> {
89 pub fn new(quota: Quota, backend: B) -> Self {
91 Self {
92 quota,
93 backend: Arc::new(backend),
94 }
95 }
96
97 pub async fn check(&self, key: &str) -> RateLimitResult {
99 self.backend.check(key, &self.quota).await
100 }
101
102 pub fn check_sync(&self, key: &str) -> RateLimitResult {
107 #[allow(clippy::expect_used)]
111 let rt = tokio::runtime::Builder::new_current_thread()
112 .build()
113 .expect("failed to create tokio runtime for sync check");
114 rt.block_on(self.check(key))
115 }
116}
117
118#[cfg(feature = "in-memory")]
119pub use keyed::KeyedRateLimiter;
120
121#[cfg(feature = "in-memory")]
122mod keyed {
123 use dashmap::DashMap;
124
125 use crate::RateLimiter;
126 use crate::backend::RateLimitBackend;
127 use crate::metrics::RateLimitResult;
128 use crate::quota::Quota;
129
130 pub struct KeyedRateLimiter<B: RateLimitBackend + Clone> {
154 limiters: DashMap<String, RateLimiter<B>>,
155 default_quota: Quota,
156 backend: std::sync::Arc<B>,
160 }
161
162 impl<B: RateLimitBackend + Clone> KeyedRateLimiter<B> {
163 pub fn new(default_quota: Quota, backend: B) -> Self {
165 Self {
166 limiters: DashMap::new(),
167 default_quota,
168 backend: std::sync::Arc::new(backend),
169 }
170 }
171
172 pub fn with_quota_for_key(&self, key: &str, quota: Quota) {
178 self.limiters.insert(
179 key.to_string(),
180 RateLimiter {
181 quota,
182 backend: std::sync::Arc::clone(&self.backend),
183 },
184 );
185 }
186
187 pub async fn check(&self, key: &str) -> RateLimitResult {
189 let limiter = self
190 .limiters
191 .entry(key.to_string())
192 .or_insert_with(|| RateLimiter {
193 quota: self.default_quota.clone(),
194 backend: std::sync::Arc::clone(&self.backend),
195 })
196 .value()
197 .clone();
198 limiter.check(key).await
199 }
200
201 pub fn check_sync(&self, key: &str) -> RateLimitResult {
206 #[allow(clippy::expect_used)]
210 let rt = tokio::runtime::Builder::new_current_thread()
211 .build()
212 .expect("failed to create tokio runtime for sync check");
213 rt.block_on(self.check(key))
214 }
215 }
216}
217
218#[allow(
222 clippy::unwrap_used,
223 clippy::expect_used,
224 clippy::indexing_slicing,
225 clippy::panic
226)]
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use std::time::Duration;
231
232 #[test]
233 fn quota_per_second() {
234 let q = Quota::per_second(10);
235 assert_eq!(q.burst, 10);
236 assert_eq!(q.interval(), Duration::from_millis(100));
237 }
238
239 #[test]
240 fn quota_per_minute() {
241 let q = Quota::per_minute(100);
242 assert_eq!(q.burst, 100);
243 assert_eq!(q.interval(), Duration::from_millis(600));
244 }
245
246 #[test]
247 fn quota_per_hour() {
248 let q = Quota::per_hour(3600);
249 assert_eq!(q.burst, 3600);
250 assert_eq!(q.interval(), Duration::from_secs(1));
251 }
252
253 #[test]
254 fn quota_allow_burst() {
255 let q = Quota::per_second(10).allow_burst(20);
256 assert_eq!(q.burst, 20);
257 }
258
259 #[test]
260 fn rate_limit_result_fields() {
261 let r = RateLimitResult {
262 allowed: true,
263 remaining: 5,
264 reset_at: std::time::Instant::now(),
265 limit: 10,
266 retry_after: None,
267 };
268 assert!(r.allowed);
269 assert_eq!(r.remaining, 5);
270 assert_eq!(r.limit, 10);
271 assert!(r.retry_after.is_none());
272 }
273
274 #[test]
275 fn rate_limit_result_headers() {
276 let r = RateLimitResult {
277 allowed: true,
278 remaining: 7,
279 reset_at: std::time::Instant::now() + Duration::from_secs(30),
280 limit: 10,
281 retry_after: None,
282 };
283 let headers = r.headers();
284 assert_eq!(headers.len(), 3);
285 assert_eq!(headers[0].0, "X-RateLimit-Limit");
286 assert_eq!(headers[0].1, "10");
287 assert_eq!(headers[1].0, "X-RateLimit-Remaining");
288 assert_eq!(headers[1].1, "7");
289 assert_eq!(headers[2].0, "X-RateLimit-Reset");
290 }
291
292 #[test]
293 fn rate_limit_result_retry_after() {
294 let r = RateLimitResult {
295 allowed: false,
296 remaining: 0,
297 reset_at: std::time::Instant::now(),
298 limit: 10,
299 retry_after: Some(Duration::from_millis(100)),
300 };
301 assert!(!r.allowed);
302 assert_eq!(r.retry_after, Some(Duration::from_millis(100)));
303 }
304
305 #[test]
306 fn in_memory_backend_new() {
307 let _ = InMemoryBackend::new();
308 }
309
310 #[test]
311 fn in_memory_backend_default() {
312 let _ = InMemoryBackend::default();
313 }
314
315 #[test]
316 fn rate_limit_error_display() {
317 let e = RateLimitError::RateLimited;
318 assert_eq!(e.to_string(), "rate limit exceeded");
319
320 let e = RateLimitError::BackendError("connection refused".into());
321 assert_eq!(
322 e.to_string(),
323 "rate limit backend error: connection refused"
324 );
325 }
326
327 #[tokio::test]
328 async fn first_request_is_allowed() {
329 let backend = InMemoryBackend::new();
330 let limiter = RateLimiter::new(Quota::per_second(10), backend);
331 let result = limiter.check("user-1").await;
332 assert!(result.allowed);
333 assert_eq!(result.remaining, 9);
334 assert_eq!(result.limit, 10);
335 assert!(result.retry_after.is_none());
336 }
337
338 #[tokio::test]
339 async fn rate_limit_exhaustion() {
340 let backend = InMemoryBackend::new();
341 let limiter = RateLimiter::new(Quota::per_second(2), backend);
342 let r1 = limiter.check("user-1").await;
343 assert!(r1.allowed);
344 let r2 = limiter.check("user-1").await;
345 assert!(r2.allowed);
346 let r3 = limiter.check("user-1").await;
347 assert!(!r3.allowed);
348 assert!(r3.retry_after.is_some());
349 }
350
351 #[tokio::test]
352 async fn keyed_limiter_default_quota() {
353 use crate::KeyedRateLimiter;
354 let backend = InMemoryBackend::new();
355 let limiter = KeyedRateLimiter::new(Quota::per_second(5), backend);
356
357 let r = limiter.check("client-1").await;
358 assert!(r.allowed);
359 assert_eq!(r.limit, 5);
360 }
361
362 #[tokio::test]
363 async fn keyed_limiter_custom_quota_per_key() {
364 use crate::KeyedRateLimiter;
365 let backend = InMemoryBackend::new();
366 let limiter = KeyedRateLimiter::new(Quota::per_second(100), backend);
367
368 limiter.with_quota_for_key("login", Quota::per_second(2));
370
371 let r_login = limiter.check("login").await;
372 assert!(r_login.allowed);
373 assert_eq!(r_login.limit, 2);
374
375 let r_api = limiter.check("api").await;
376 assert!(r_api.allowed);
377 assert_eq!(r_api.limit, 100);
378 }
379
380 #[tokio::test]
381 async fn keyed_limiter_isolation() {
382 use crate::KeyedRateLimiter;
383 let backend = InMemoryBackend::new();
384 let limiter = KeyedRateLimiter::new(Quota::per_second(1), backend);
385
386 let r1 = limiter.check("a").await;
387 assert!(r1.allowed);
388
389 let r1_again = limiter.check("a").await;
391 assert!(!r1_again.allowed);
392
393 let r2 = limiter.check("b").await;
394 assert!(r2.allowed);
395 }
396
397 #[test]
398 fn keyed_limiter_sync_check() {
399 use crate::KeyedRateLimiter;
400 let backend = InMemoryBackend::new();
401 let limiter = KeyedRateLimiter::new(Quota::per_second(10), backend);
402
403 let r = limiter.check_sync("client-1");
404 assert!(r.allowed);
405 assert_eq!(r.limit, 10);
406 }
407
408 #[tokio::test]
409 async fn keyed_limiter_headers() {
410 use crate::KeyedRateLimiter;
411 let backend = InMemoryBackend::new();
412 let limiter = KeyedRateLimiter::new(Quota::per_second(10), backend);
413
414 let r = limiter.check("client-1").await;
415 let headers = r.headers();
416 assert!(headers.iter().any(|(k, _)| *k == "X-RateLimit-Limit"));
417 assert!(
418 headers
419 .iter()
420 .any(|(k, v)| *k == "X-RateLimit-Remaining" && v == "9")
421 );
422 }
423
424 #[test]
425 fn sync_check_basic() {
426 let backend = InMemoryBackend::new();
427 let limiter = RateLimiter::new(Quota::per_second(10), backend);
428 let r = limiter.check_sync("user-1");
429 assert!(r.allowed);
430 }
431}