1use crate::error::{Error, Result};
24use crate::middleware::{Middleware, Next};
25use crate::request::Request;
26use crate::response::Response;
27use async_trait::async_trait;
28use std::collections::HashMap;
29use std::sync::{Arc, Mutex};
30use std::time::{Duration, Instant};
31
32type KeyFn = dyn Fn(&Request) -> Option<String> + Send + Sync;
33
34struct Window {
35 started: Instant,
36 count: u32,
37 prev: u32,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct Decision {
44 pub allowed: bool,
45 pub limit: u32,
46 pub remaining: u32,
47 pub reset_secs: u64,
49}
50
51#[async_trait]
57pub trait RateLimitStore: Send + Sync + 'static {
58 async fn hit(&self, key: &str, limit: u32, window: Duration, sliding: bool) -> Decision;
60}
61
62pub struct RateLimiter {
64 store: Option<Arc<dyn RateLimitStore>>,
65 limit: u32,
66 window: Duration,
67 max_keys: usize,
68 sliding: bool,
69 key_fn: Arc<KeyFn>,
70 state: Mutex<HashMap<String, Window>>,
71}
72
73impl RateLimiter {
74 pub fn new(limit: u32, window: Duration) -> Self {
76 Self {
77 store: None,
78 limit,
79 window,
80 max_keys: 100_000,
81 sliding: false,
82 key_fn: Arc::new(|req| req.client_ip().map(|ip| ip.to_string())),
83 state: Mutex::new(HashMap::new()),
84 }
85 }
86
87 pub fn store(mut self, store: impl RateLimitStore) -> Self {
90 self.store = Some(Arc::new(store));
91 self
92 }
93
94 pub fn sliding_window(mut self) -> Self {
100 self.sliding = true;
101 self
102 }
103
104 pub fn key_by<F>(mut self, f: F) -> Self
107 where
108 F: Fn(&Request) -> Option<String> + Send + Sync + 'static,
109 {
110 self.key_fn = Arc::new(f);
111 self
112 }
113
114 pub fn trust_forwarded_for(self) -> Self {
122 self.key_by(|req| {
123 req.headers
124 .get("x-forwarded-for")
125 .and_then(|v| v.to_str().ok())
126 .and_then(|v| v.split(',').next())
127 .map(|ip| ip.trim().to_string())
128 .filter(|ip| !ip.is_empty())
129 .or_else(|| req.remote_addr.map(|a| a.ip().to_string()))
130 })
131 }
132
133 pub fn max_keys(mut self, max_keys: usize) -> Self {
136 self.max_keys = max_keys;
137 self
138 }
139
140 pub fn check(&self, key: &str, now: Instant) -> Decision {
142 let mut map = self.state.lock().unwrap_or_else(|e| e.into_inner());
143
144 if map.len() >= self.max_keys && !map.contains_key(key) {
145 let window = self.window;
146 map.retain(|_, w| now.duration_since(w.started) < window);
147 if map.len() >= self.max_keys {
148 return Decision {
149 allowed: true,
150 limit: self.limit,
151 remaining: self.limit,
152 reset_secs: self.window.as_secs().max(1),
153 };
154 }
155 }
156
157 let w = map.entry(key.to_string()).or_insert(Window {
158 started: now,
159 count: 0,
160 prev: 0,
161 });
162 if self.sliding {
163 return self.check_sliding(w, now);
164 }
165 if now.duration_since(w.started) >= self.window {
166 *w = Window {
167 started: now,
168 count: 0,
169 prev: 0,
170 };
171 }
172 let elapsed = now.duration_since(w.started);
173 let reset_secs = self
174 .window
175 .saturating_sub(elapsed)
176 .as_secs_f64()
177 .ceil()
178 .max(1.0) as u64;
179
180 if w.count < self.limit {
181 w.count += 1;
182 Decision {
183 allowed: true,
184 limit: self.limit,
185 remaining: self.limit - w.count,
186 reset_secs,
187 }
188 } else {
189 Decision {
190 allowed: false,
191 limit: self.limit,
192 remaining: 0,
193 reset_secs,
194 }
195 }
196 }
197
198 fn check_sliding(&self, w: &mut Window, now: Instant) -> Decision {
201 let win = self.window.as_secs_f64().max(f64::EPSILON);
202 let mut elapsed = now.duration_since(w.started).as_secs_f64();
203 if elapsed >= win {
204 let periods = (elapsed / win).floor();
205 w.prev = if periods < 2.0 { w.count } else { 0 };
206 w.count = 0;
207 w.started += Duration::from_secs_f64(periods * win);
208 elapsed = now.duration_since(w.started).as_secs_f64();
209 }
210 let weight = 1.0 - elapsed / win;
211 let estimate = f64::from(w.prev) * weight + f64::from(w.count);
212 let limit = f64::from(self.limit);
213
214 if estimate + 1.0 <= limit {
215 w.count += 1;
216 let used = estimate + 1.0;
217 return Decision {
218 allowed: true,
219 limit: self.limit,
220 remaining: (limit - used).floor().max(0.0) as u32,
221 reset_secs: (win - elapsed).ceil().max(1.0) as u64,
222 };
223 }
224 let wait = if w.prev > 0 && f64::from(w.count) < limit {
226 let need = estimate + 1.0 - limit;
228 (need * win / f64::from(w.prev)).min(win - elapsed)
229 } else {
230 win - elapsed
231 };
232 Decision {
233 allowed: false,
234 limit: self.limit,
235 remaining: 0,
236 reset_secs: wait.ceil().max(1.0) as u64,
237 }
238 }
239}
240
241#[async_trait]
242impl Middleware for RateLimiter {
243 async fn handle(&self, req: Request, next: Next) -> Result<Response> {
244 if req.method == http::Method::OPTIONS {
246 return next.run(req).await;
247 }
248 let Some(key) = (self.key_fn)(&req) else {
249 return next.run(req).await;
250 };
251
252 let decision = match &self.store {
253 Some(store) => store.hit(&key, self.limit, self.window, self.sliding).await,
254 None => self.check(&key, Instant::now()),
255 };
256 let res = if decision.allowed {
257 match next.run(req).await {
258 Ok(res) => res,
259 Err(err) => Response::from_error(err),
260 }
261 } else {
262 Response::from_error(Error::custom(429, "Too many requests"))
263 .with_header("retry-after", decision.reset_secs.to_string())
264 };
265
266 Ok(res
267 .with_header("x-ratelimit-limit", decision.limit.to_string())
268 .with_header("x-ratelimit-remaining", decision.remaining.to_string())
269 .with_header("x-ratelimit-reset", decision.reset_secs.to_string()))
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn counts_within_window_and_resets_after() {
279 let rl = RateLimiter::new(2, Duration::from_secs(10));
280 let t0 = Instant::now();
281 let d1 = rl.check("a", t0);
282 assert_eq!((d1.allowed, d1.remaining), (true, 1));
283 assert_eq!(rl.check("a", t0 + Duration::from_secs(1)).remaining, 0);
284
285 let blocked = rl.check("a", t0 + Duration::from_millis(2500));
286 assert!(!blocked.allowed);
287 assert_eq!((blocked.remaining, blocked.reset_secs), (0, 8));
288
289 let again = rl.check("a", t0 + Duration::from_secs(10));
290 assert!(again.allowed && again.remaining == 1);
291 }
292
293 #[test]
294 fn keys_are_independent() {
295 let rl = RateLimiter::new(1, Duration::from_secs(60));
296 let t = Instant::now();
297 assert!(rl.check("a", t).allowed);
298 assert!(!rl.check("a", t).allowed);
299 assert!(rl.check("b", t).allowed);
300 }
301
302 #[test]
303 fn full_table_drops_expired_then_fails_open() {
304 let rl = RateLimiter::new(1, Duration::from_secs(10)).max_keys(2);
305 let t = Instant::now();
306 rl.check("a", t);
307 rl.check("b", t);
308 assert!(rl.check("c", t).allowed);
310 assert!(rl.check("c", t).allowed);
311 assert!(!rl.check("a", t).allowed);
312 let later = t + Duration::from_secs(11);
314 assert!(rl.check("c", later).allowed);
315 assert!(!rl.check("c", later).allowed);
316 }
317
318 #[test]
319 fn sliding_window_has_no_boundary_burst() {
320 let t0 = Instant::now();
321 let secs = Duration::from_secs;
322 let at = |s: f64| t0 + Duration::from_secs_f64(s);
323 let fixed = RateLimiter::new(10, secs(10));
325 assert!(fixed.check("a", t0).allowed);
326 for _ in 0..9 {
327 assert!(fixed.check("a", at(9.0)).allowed);
328 }
329 let burst = (0..10)
330 .filter(|_| fixed.check("a", at(10.5)).allowed)
331 .count();
332 assert_eq!(burst, 10);
333
334 let sliding = RateLimiter::new(10, secs(10)).sliding_window();
336 assert!(sliding.check("a", t0).allowed);
337 for _ in 0..9 {
338 assert!(sliding.check("a", at(9.0)).allowed);
339 }
340 let allowed = (0..10)
341 .filter(|_| sliding.check("a", at(10.5)).allowed)
342 .count();
343 assert_eq!(allowed, 0);
344 }
345
346 #[test]
347 fn sliding_window_recovers_gradually_and_reports_when() {
348 let rl = RateLimiter::new(10, Duration::from_secs(10)).sliding_window();
349 let t0 = Instant::now();
350 for _ in 0..10 {
351 assert!(rl.check("k", t0).allowed);
352 }
353 let blocked = rl.check("k", t0 + Duration::from_secs(1));
354 assert!(!blocked.allowed);
355 assert!(blocked.reset_secs >= 1 && blocked.reset_secs <= 10);
356
357 let later = rl.check("k", t0 + Duration::from_secs(25));
359 assert!(later.allowed);
360 assert_eq!(later.remaining, 9);
361
362 let rl = RateLimiter::new(10, Duration::from_secs(10)).sliding_window();
364 for _ in 0..10 {
365 rl.check("k", t0);
366 }
367 let allowed = (0..10)
368 .filter(|_| rl.check("k", t0 + Duration::from_secs(15)).allowed)
369 .count();
370 assert_eq!(allowed, 5);
371 }
372
373 #[test]
374 fn sliding_remaining_counts_down() {
375 let rl = RateLimiter::new(3, Duration::from_secs(60)).sliding_window();
376 let t = Instant::now();
377 assert_eq!(rl.check("a", t).remaining, 2);
378 assert_eq!(rl.check("a", t).remaining, 1);
379 assert_eq!(rl.check("a", t).remaining, 0);
380 assert!(!rl.check("a", t).allowed);
381 }
382
383 #[test]
384 fn zero_limit_blocks_everything() {
385 let rl = RateLimiter::new(0, Duration::from_secs(1));
386 assert!(!rl.check("a", Instant::now()).allowed);
387 }
388}