1pub mod bucket;
2
3use super::Middleware;
4use crate::error::{AuthError, AuthResult};
5use crate::types::{AuthRequest, AuthResponse};
6use async_trait::async_trait;
7use indexmap::IndexMap;
8use std::collections::{BTreeSet, HashMap};
9use std::sync::{Arc, Mutex};
10use std::time::{Duration, Instant};
11
12#[derive(Debug)]
14enum PathPattern {
15 Exact,
16 Glob(regex::Regex),
17 Invalid,
18}
19
20impl PathPattern {
21 fn new(pattern: &str) -> Self {
22 if !pattern.contains('*') {
23 return Self::Exact;
24 }
25 crate::utils::wildcard::compile_path_glob(pattern).map_or(Self::Invalid, Self::Glob)
26 }
27
28 fn matches(&self, pattern: &str, path: &str) -> AuthResult<bool> {
29 match self {
30 Self::Exact => Ok(pattern == path),
31 Self::Glob(compiled) => Ok(compiled.is_match(path)),
32 Self::Invalid => Err(AuthError::internal("Invalid rate-limit path pattern")),
33 }
34 }
35}
36
37#[async_trait]
39pub trait RateLimitStorage: Send + Sync + std::fmt::Debug {
40 fn observe_window(&self, _window_seconds: f64) {}
43
44 async fn consume(&self, key: &str, rule: &EndpointRateLimit) -> AuthResult<RateLimitDecision>;
45}
46
47#[derive(Debug, Clone, Copy)]
48pub enum RateLimitDecision {
49 Allowed,
50 Blocked { retry_after: f64 },
51}
52
53#[derive(Debug)]
56pub struct MemoryRateLimitStorage {
57 state: Mutex<RateLimitState>,
58 max_buckets: usize,
59}
60
61impl MemoryRateLimitStorage {
62 #[must_use]
63 pub fn new(max_buckets: usize) -> Self {
64 Self {
65 state: Mutex::new(RateLimitState::default()),
66 max_buckets,
67 }
68 }
69 fn consume_memory(
70 &self,
71 key: String,
72 limit: &EndpointRateLimit,
73 ) -> AuthResult<RateLimitDecision> {
74 let now = Instant::now();
75 let deadline = Duration::try_from_secs_f64(limit.window_seconds)
76 .ok()
77 .and_then(|window| now.checked_add(window));
78 let mut state = self
79 .state
80 .lock()
81 .map_err(|_| AuthError::internal("Rate-limit lock poisoned"))?;
82 while state
83 .expirations
84 .first()
85 .is_some_and(|(expiry, _)| *expiry <= now)
86 {
87 if let Some((_, expired_key)) = state.expirations.pop_first() {
88 _ = state.buckets.remove(&expired_key);
89 }
90 }
91 if limit.window_seconds <= 0.0 || limit.window_seconds.is_nan() {
93 if let Some(previous) = state.buckets.remove(&key)
94 && let Some(deadline) = previous.deadline
95 {
96 _ = state.expirations.remove(&(deadline, key));
97 }
98 return Ok(RateLimitDecision::Allowed);
99 }
100 let count = if let Some(previous) = state.buckets.get(&key).copied() {
101 let elapsed = now.duration_since(previous.last_request).as_secs_f64();
102 let expired = elapsed >= limit.window_seconds;
103 if !expired && previous.count >= limit.max_requests {
104 return Ok(RateLimitDecision::Blocked {
105 retry_after: (limit.window_seconds
106 - now.duration_since(previous.last_request).as_secs_f64())
107 .ceil(),
108 });
109 }
110 if let Some(deadline) = previous.deadline {
111 _ = state.expirations.remove(&(deadline, key.clone()));
112 }
113 if expired { 1.0 } else { previous.count + 1.0 }
114 } else {
115 if state.buckets.len() >= self.max_buckets {
116 let remaining = state
117 .expirations
118 .first()
119 .map_or(limit.window_seconds, |(expiry, _)| {
120 expiry.saturating_duration_since(now).as_secs_f64()
121 });
122 return Ok(RateLimitDecision::Blocked {
123 retry_after: remaining.ceil(),
124 });
125 }
126 1.0
127 };
128 _ = state.buckets.insert(
129 key.clone(),
130 Bucket {
131 deadline,
132 last_request: now,
133 count,
134 },
135 );
136 if let Some(deadline) = deadline {
137 _ = state.expirations.insert((deadline, key));
138 }
139 Ok(RateLimitDecision::Allowed)
140 }
141}
142
143#[async_trait]
144impl RateLimitStorage for MemoryRateLimitStorage {
145 async fn consume(&self, key: &str, rule: &EndpointRateLimit) -> AuthResult<RateLimitDecision> {
146 self.consume_memory(key.to_owned(), rule)
147 }
148}
149
150pub struct CacheRateLimitStorage {
153 cache: Arc<dyn crate::store::secondary_storage::CacheAdapter>,
154}
155
156impl std::fmt::Debug for CacheRateLimitStorage {
157 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 formatter
159 .debug_struct("CacheRateLimitStorage")
160 .finish_non_exhaustive()
161 }
162}
163
164impl CacheRateLimitStorage {
165 #[must_use]
166 pub fn new(cache: Arc<dyn crate::store::secondary_storage::CacheAdapter>) -> Self {
167 Self { cache }
168 }
169}
170
171#[async_trait]
172impl RateLimitStorage for CacheRateLimitStorage {
173 async fn consume(&self, key: &str, rule: &EndpointRateLimit) -> AuthResult<RateLimitDecision> {
174 let ttl = Duration::try_from_secs_f64(rule.window_seconds)
175 .map_err(|_| AuthError::internal("Invalid shared rate-limit window"))?;
176 let count = self
177 .cache
178 .increment(key, ttl)
179 .await
180 .map_err(AuthError::into_callback_failure)?;
181 Ok(if count <= rule.max_requests {
182 RateLimitDecision::Allowed
183 } else {
184 RateLimitDecision::Blocked {
185 retry_after: rule.window_seconds,
186 }
187 })
188 }
189}
190
191#[async_trait]
193pub trait RateLimitResolver: Send + Sync + std::fmt::Debug {
194 async fn resolve(
195 &self,
196 request: &AuthRequest,
197 inherited: &EndpointRateLimit,
198 ) -> AuthResult<Option<EndpointRateLimit>>;
199}
200
201#[derive(Debug, Clone)]
202pub enum RateLimitRule {
203 Limit(EndpointRateLimit),
204 Disabled,
205 Dynamic(Arc<dyn RateLimitResolver>),
206}
207
208#[derive(Debug, Clone)]
210pub struct PluginRateLimit {
211 pub matches: fn(&str) -> bool,
212 pub limit: EndpointRateLimit,
213}
214
215#[derive(Debug, Clone)]
217pub struct RateLimitConfig {
218 pub default: EndpointRateLimit,
220
221 pub per_endpoint: IndexMap<String, RateLimitRule>,
223
224 pub storage: Option<Arc<dyn RateLimitStorage>>,
226
227 pub max_buckets: usize,
229
230 pub enabled: bool,
232}
233
234#[derive(Debug, Clone)]
236pub struct EndpointRateLimit {
237 pub window_seconds: f64,
239
240 pub max_requests: f64,
242}
243
244impl Default for RateLimitConfig {
245 fn default() -> Self {
246 Self {
247 default: EndpointRateLimit {
248 window_seconds: 10.0,
249 max_requests: 100.0,
250 },
251 per_endpoint: IndexMap::new(),
252 storage: None,
253 max_buckets: 100_000,
254 enabled: true,
255 }
256 }
257}
258
259impl RateLimitConfig {
260 #[must_use]
261 pub fn new() -> Self {
262 Self::default()
263 }
264
265 #[must_use]
266 pub fn default_limit(mut self, window: Duration, max_requests: u32) -> Self {
267 self.default = EndpointRateLimit {
268 window_seconds: window.as_secs_f64(),
269 max_requests: f64::from(max_requests),
270 };
271 self
272 }
273
274 #[must_use]
275 pub fn endpoint(
276 mut self,
277 path: impl Into<String>,
278 window: Duration,
279 max_requests: u32,
280 ) -> Self {
281 _ = self.per_endpoint.insert(
282 path.into(),
283 RateLimitRule::Limit(EndpointRateLimit {
284 window_seconds: window.as_secs_f64(),
285 max_requests: f64::from(max_requests),
286 }),
287 );
288 self
289 }
290
291 #[must_use]
292 pub fn rule(mut self, path: impl Into<String>, rule: RateLimitRule) -> Self {
293 _ = self.per_endpoint.insert(path.into(), rule);
294 self
295 }
296
297 #[must_use]
298 pub fn storage(mut self, storage: Arc<dyn RateLimitStorage>) -> Self {
299 self.storage = Some(storage);
300 self
301 }
302
303 #[must_use]
305 pub const fn max_buckets(mut self, maximum: usize) -> Self {
306 self.max_buckets = maximum;
307 self
308 }
309
310 #[must_use]
311 pub const fn enabled(mut self, enabled: bool) -> Self {
312 self.enabled = enabled;
313 self
314 }
315}
316
317#[derive(Debug)]
324pub struct RateLimitMiddleware {
325 config: RateLimitConfig,
326 endpoint_patterns: Vec<PathPattern>,
327 base_path: String,
328 memory: MemoryRateLimitStorage,
329 plugin_rules: Vec<PluginRateLimit>,
330}
331
332#[derive(Debug, Default)]
333struct RateLimitState {
334 buckets: HashMap<String, Bucket>,
335 expirations: BTreeSet<(Instant, String)>,
336}
337
338#[derive(Debug, Clone, Copy)]
339struct Bucket {
340 deadline: Option<Instant>,
341 last_request: Instant,
342 count: f64,
343}
344
345const AUTH_LIMIT: EndpointRateLimit = EndpointRateLimit {
346 window_seconds: 10.0,
347 max_requests: 3.0,
348};
349const EMAIL_LIMIT: EndpointRateLimit = EndpointRateLimit {
350 window_seconds: 60.0,
351 max_requests: 3.0,
352};
353
354impl RateLimitMiddleware {
355 #[must_use]
356 pub fn new(mut config: RateLimitConfig) -> Self {
357 if config.default.window_seconds == 0.0 || config.default.window_seconds.is_nan() {
358 config.default.window_seconds = 10.0;
359 }
360 if config.default.max_requests == 0.0 || config.default.max_requests.is_nan() {
361 config.default.max_requests = 100.0;
362 }
363 let memory = MemoryRateLimitStorage::new(config.max_buckets);
364 if let Some(storage) = &config.storage {
365 storage.observe_window(config.default.window_seconds);
366 storage.observe_window(AUTH_LIMIT.window_seconds);
367 storage.observe_window(EMAIL_LIMIT.window_seconds);
368 for rule in config.per_endpoint.values() {
369 if let RateLimitRule::Limit(limit) = rule {
370 storage.observe_window(limit.window_seconds);
371 }
372 }
373 }
374 let endpoint_patterns = config
375 .per_endpoint
376 .keys()
377 .map(|pattern| PathPattern::new(pattern))
378 .collect();
379 Self {
380 config,
381 endpoint_patterns,
382 base_path: String::new(),
383 memory,
384 plugin_rules: Vec::new(),
385 }
386 }
387
388 #[must_use]
390 pub fn with_base_path(mut self, path: impl Into<String>) -> Self {
391 path.into()
392 .trim_end_matches('/')
393 .clone_into(&mut self.base_path);
394 self
395 }
396
397 fn client_key(req: &AuthRequest) -> String {
400 let policy = req.extensions().get::<crate::config::IpAddressConfig>();
401 policy
402 .as_ref()
403 .map_or_else(
404 || crate::config::IpAddressConfig::default().resolve_ip(&req.headers),
405 |policy| policy.resolve_ip(&req.headers),
406 )
407 .unwrap_or_else(|| "no-trusted-ip".to_owned())
408 }
409
410 #[must_use]
411 pub fn with_plugin_rules(mut self, rules: Vec<PluginRateLimit>) -> Self {
412 if let Some(storage) = &self.config.storage {
413 for rule in &rules {
414 storage.observe_window(rule.limit.window_seconds);
415 }
416 }
417 self.plugin_rules = rules;
418 self
419 }
420
421 async fn limit_for_request(
422 &self,
423 req: &AuthRequest,
424 path: &str,
425 ) -> AuthResult<Option<EndpointRateLimit>> {
426 let limit = self
427 .plugin_rules
428 .iter()
429 .find(|rule| (rule.matches)(path))
430 .map_or_else(
431 || self.default_limit_for_path(path).clone(),
432 |rule| rule.limit.clone(),
433 );
434 for ((pattern, rule), compiled) in
435 self.config.per_endpoint.iter().zip(&self.endpoint_patterns)
436 {
437 if compiled.matches(pattern, path)? {
438 return match rule {
439 RateLimitRule::Limit(limit) => Ok(Some(limit.clone())),
440 RateLimitRule::Disabled => Ok(None),
441 RateLimitRule::Dynamic(resolver) => resolver.resolve(req, &limit).await,
442 };
443 }
444 }
445 Ok(Some(limit))
446 }
447
448 fn default_limit_for_path(&self, path: &str) -> &EndpointRateLimit {
449 if ["/sign-in", "/sign-up", "/change-password", "/change-email"]
450 .iter()
451 .any(|prefix| path.starts_with(prefix))
452 {
453 return &AUTH_LIMIT;
454 }
455 if matches!(
456 path,
457 "/request-password-reset"
458 | "/send-verification-email"
459 | "/email-otp/send-verification-otp"
460 | "/email-otp/request-password-reset"
461 ) || path.starts_with("/forget-password")
462 {
463 return &EMAIL_LIMIT;
464 }
465 &self.config.default
466 }
467
468 fn blocked(retry_after: f64) -> AuthResponse {
469 AuthResponse::text(
470 429,
471 r#"{"message":"Too many requests. Please try again later."}"#,
472 )
473 .with_header("content-type", "application/json")
474 .with_header(
475 "X-Retry-After",
476 ryu_js::Buffer::new().format(retry_after).to_owned(),
477 )
478 }
479}
480
481#[async_trait]
482impl Middleware for RateLimitMiddleware {
483 fn name(&self) -> &'static str {
484 "rate-limit"
485 }
486
487 async fn before_request(&self, req: &AuthRequest) -> AuthResult<Option<AuthResponse>> {
488 if !self.config.enabled
489 || req
490 .extensions()
491 .get::<crate::config::IpAddressConfig>()
492 .is_some_and(|policy| policy.disable_ip_tracking)
493 {
494 return Ok(None);
495 }
496
497 let requested = req.path.trim_end_matches('/');
498 let path = if requested == self.base_path || requested.is_empty() {
499 "/"
500 } else {
501 requested
502 .strip_prefix(&self.base_path)
503 .filter(|suffix| suffix.starts_with('/'))
504 .unwrap_or(requested)
505 };
506 let Some(limit) = self.limit_for_request(req, path).await? else {
507 return Ok(None);
508 };
509 let ip = Self::client_key(req);
510 let decision = if let Some(storage) = &self.config.storage {
511 storage.consume(&format!("{ip}|{path}"), &limit).await?
512 } else {
513 self.memory.consume(&format!("{ip}|{path}"), &limit).await?
514 };
515 Ok(match decision {
516 RateLimitDecision::Allowed => None,
517 RateLimitDecision::Blocked { retry_after } => Some(Self::blocked(retry_after)),
518 })
519 }
520}
521
522#[cfg(test)]
524mod tests {
525 use super::*;
526 use crate::types::HttpMethod;
527 use std::collections::HashMap as StdHashMap;
528
529 fn make_request(path: &str, ip: &str) -> AuthRequest {
530 let mut headers = StdHashMap::new();
531 headers.insert("x-forwarded-for".to_owned(), ip.to_owned());
532 AuthRequest::from_parts(
533 HttpMethod::Post,
534 path.to_owned(),
535 headers,
536 None,
537 StdHashMap::new(),
538 )
539 }
540
541 #[tokio::test]
543 async fn test_rate_limit_allows_within_limit() {
544 let config = RateLimitConfig::new().default_limit(Duration::from_secs(60), 5);
545 let mw = RateLimitMiddleware::new(config);
546 let req = make_request("/get-session", "1.2.3.4");
547
548 for _ in 0..5 {
549 assert!(mw.before_request(&req).await.unwrap().is_none());
550 }
551 }
552
553 #[tokio::test]
555 async fn test_rate_limit_blocks_over_limit() {
556 let config = RateLimitConfig::new().default_limit(Duration::from_secs(60), 3);
557 let mw = RateLimitMiddleware::new(config);
558 let req = make_request("/get-session", "1.2.3.4");
559
560 for _ in 0..3 {
561 assert!(mw.before_request(&req).await.unwrap().is_none());
562 }
563
564 let resp = mw.before_request(&req).await.unwrap();
565 assert!(resp.is_some());
566 assert_eq!(resp.unwrap().status, 429);
567 }
568
569 #[tokio::test]
571 async fn test_rate_limit_per_client() {
572 let config = RateLimitConfig::new().default_limit(Duration::from_secs(60), 2);
573 let mw = RateLimitMiddleware::new(config);
574
575 let req_a = make_request("/get-session", "1.1.1.1");
576 let req_b = make_request("/get-session", "2.2.2.2");
577
578 for _ in 0..2 {
580 assert!(mw.before_request(&req_a).await.unwrap().is_none());
581 }
582 assert!(mw.before_request(&req_a).await.unwrap().is_some());
583
584 assert!(mw.before_request(&req_b).await.unwrap().is_none());
586 }
587
588 #[tokio::test]
590 async fn test_rate_limit_per_endpoint_override() {
591 for (pattern, path, matches) in [
592 ("/sign-in/email", "/sign-in/email", true),
593 ("/sign-in/*", "/sign-in/email", true),
594 ("/email-otp/*", "/email-otp/send-verification-otp", true),
595 ("/email-otp/*", "/email-otp/nested/send", true),
596 ("/literal/?", "/literal/q", false),
597 ] {
598 let config = RateLimitConfig::new()
599 .default_limit(Duration::from_secs(60), 100)
600 .endpoint(pattern, Duration::from_secs(60), 2);
601 let mw = RateLimitMiddleware::new(config);
602 let req = make_request(path, "1.2.3.4");
603 for _ in 0..2 {
604 assert!(mw.before_request(&req).await.unwrap().is_none());
605 }
606 let response = mw.before_request(&req).await.unwrap();
607 assert_eq!(
608 response.as_ref().map(|response| response.status),
609 matches.then_some(429)
610 );
611 }
612 }
613
614 #[tokio::test]
616 async fn test_rate_limit_disabled() {
617 let config = RateLimitConfig::new()
618 .default_limit(Duration::from_secs(60), 1)
619 .enabled(false);
620 let mw = RateLimitMiddleware::new(config);
621 let req = make_request("/sign-in/email", "1.2.3.4");
622
623 for _ in 0..10 {
624 assert!(mw.before_request(&req).await.unwrap().is_none());
625 }
626 }
627
628 #[tokio::test]
630 async fn default_sensitive_routes_are_limited_before_generic_routes() {
631 for path in [
632 "/sign-in/email",
633 "/sign-up/email",
634 "/change-password",
635 "/change-email",
636 "/request-password-reset",
637 "/send-verification-email",
638 "/forget-password/email",
639 "/email-otp/send-verification-otp",
640 "/email-otp/request-password-reset",
641 ] {
642 let mw = RateLimitMiddleware::new(RateLimitConfig::new());
643 let req = make_request(path, "192.0.2.1");
644 for _ in 0..3 {
645 assert!(mw.before_request(&req).await.unwrap().is_none());
646 }
647 assert_eq!(
648 mw.before_request(&req).await.unwrap().unwrap().status,
649 429,
650 "{path}"
651 );
652 }
653 let mw = RateLimitMiddleware::new(RateLimitConfig::new());
654 let req = make_request("/get-session", "192.0.2.1");
655 for _ in 0..100 {
656 assert!(mw.before_request(&req).await.unwrap().is_none());
657 }
658 let response = mw.before_request(&req).await.unwrap().unwrap();
659 assert_eq!(response.status, 429);
660 assert!(
661 response
662 .headers
663 .get("x-retry-after")
664 .unwrap()
665 .parse::<u64>()
666 .unwrap()
667 <= 10
668 );
669 }
670
671 #[tokio::test]
673 async fn bounded_buckets_preserve_active_quotas_and_recover_after_expiry() {
674 let mw = RateLimitMiddleware::new(
675 RateLimitConfig::new()
676 .default_limit(Duration::from_millis(250), 2)
677 .max_buckets(2),
678 );
679 let first = make_request("/get-session", "192.0.2.1");
680 let second = make_request("/get-session", "192.0.2.2");
681 let third = make_request("/get-session", "192.0.2.3");
682 assert!(mw.before_request(&first).await.unwrap().is_none());
683 assert!(mw.before_request(&second).await.unwrap().is_none());
684 for _ in 0..8 {
685 assert_eq!(
686 mw.before_request(&third).await.unwrap().unwrap().status,
687 429
688 );
689 }
690 assert!(mw.before_request(&first).await.unwrap().is_none());
691 assert_eq!(
692 mw.before_request(&first).await.unwrap().unwrap().status,
693 429
694 );
695 tokio::time::sleep(Duration::from_millis(300)).await;
696 assert!(mw.before_request(&third).await.unwrap().is_none());
697 assert!(mw.before_request(&first).await.unwrap().is_none());
698 }
699
700 #[tokio::test]
701 async fn mounted_and_internal_paths_share_sensitive_route_quotas() {
702 let mw = RateLimitMiddleware::new(RateLimitConfig::new()).with_base_path("/api/auth/");
703 let mounted = make_request("/api/auth/sign-in/email", "192.0.2.1");
704 let internal = make_request("/sign-in/email", "192.0.2.1");
705 assert!(mw.before_request(&mounted).await.unwrap().is_none());
706 assert!(mw.before_request(&internal).await.unwrap().is_none());
707 assert!(mw.before_request(&mounted).await.unwrap().is_none());
708 assert_eq!(
709 mw.before_request(&internal).await.unwrap().unwrap().status,
710 429
711 );
712 let trailing = make_request("/api/auth/sign-in/email///", "192.0.2.1");
713 assert_eq!(
714 mw.before_request(&trailing).await.unwrap().unwrap().status,
715 429
716 );
717 let other_mount = make_request("/unrelated/sign-in/email", "192.0.2.1");
718 for _ in 0..4 {
719 assert!(mw.before_request(&other_mount).await.unwrap().is_none());
720 }
721 }
722
723 #[tokio::test]
724 async fn raw_numeric_limits_preserve_admission_and_retry_headers() {
725 for (window, max, expected) in [
726 (60.0, 0.0, [200, 429, 429]),
727 (60.0, -1.0, [200, 429, 429]),
728 (60.0, 1.5, [200, 200, 429]),
729 (0.0, 1.0, [200, 200, 200]),
730 (-1.0, 1.0, [200, 200, 200]),
731 (f64::NAN, 1.0, [200, 200, 200]),
732 (60.0, f64::NAN, [200, 200, 200]),
733 (f64::INFINITY, 1.0, [200, 429, 429]),
734 ] {
735 let middleware = RateLimitMiddleware::new(RateLimitConfig::new().rule(
736 "/get-session",
737 RateLimitRule::Limit(EndpointRateLimit {
738 window_seconds: window,
739 max_requests: max,
740 }),
741 ));
742 let request = make_request("/get-session", "198.51.100.10");
743 for status in expected {
744 let response = middleware.before_request(&request).await.unwrap();
745 assert_eq!(
746 response.as_ref().map_or(200, |response| response.status),
747 status
748 );
749 if let Some(response) = response {
750 assert_eq!(
751 response.headers.get("x-retry-after").unwrap(),
752 if window.is_infinite() {
753 "Infinity"
754 } else {
755 "60"
756 }
757 );
758 }
759 }
760 }
761 }
762
763 #[tokio::test]
764 async fn independent_instances_share_atomic_memory_quota_and_expiry() {
765 let storage = Arc::new(MemoryRateLimitStorage::new(100));
766 let config = RateLimitConfig::new()
767 .default_limit(Duration::from_millis(250), 3)
768 .storage(storage);
769 let instances = [
770 Arc::new(RateLimitMiddleware::new(config.clone())),
771 Arc::new(RateLimitMiddleware::new(config)),
772 ];
773 let mut tasks = tokio::task::JoinSet::new();
774 for index in 0..32 {
775 let middleware = instances.get(index % 2).unwrap().clone();
776 _ = tasks.spawn(async move {
777 middleware
778 .before_request(&make_request("/get-session", "198.51.100.11"))
779 .await
780 .unwrap()
781 });
782 }
783 let mut admitted = 0;
784 while let Some(result) = tasks.join_next().await {
785 if result.unwrap().is_none() {
786 admitted += 1;
787 }
788 }
789 assert_eq!(admitted, 3);
790 tokio::time::sleep(Duration::from_millis(300)).await;
791 assert!(
792 instances
793 .first()
794 .unwrap()
795 .before_request(&make_request("/get-session", "198.51.100.11"))
796 .await
797 .unwrap()
798 .is_none()
799 );
800 }
801
802 #[cfg(feature = "redis-cache")]
803 #[tokio::test]
804 #[ignore = "requires an isolated Redis server"]
805 async fn independent_redis_instances_share_fixed_ttl_and_backend_errors_fail_closed() {
806 use crate::store::secondary_storage::{CacheAdapter, RedisAdapter};
807 let url = std::env::var("TEST_RATE_LIMIT_REDIS_URL").unwrap();
808 let first = Arc::new(RedisAdapter::new(&url).await.unwrap());
809 let second = Arc::new(RedisAdapter::new(&url).await.unwrap());
810 let storage = [
811 CacheRateLimitStorage::new(first.clone()),
812 CacheRateLimitStorage::new(second.clone()),
813 ];
814 let rule = EndpointRateLimit {
815 window_seconds: 1.0,
816 max_requests: 3.0,
817 };
818 let key = format!("198.51.100.12|/get-session-{}", uuid::Uuid::new_v4());
819 let mut tasks = tokio::task::JoinSet::new();
820 for index in 0..24 {
821 let adapter = if index % 2 == 0 {
822 first.clone()
823 } else {
824 second.clone()
825 };
826 let key = key.clone();
827 _ = tasks.spawn(async move {
828 CacheRateLimitStorage::new(adapter)
829 .consume(
830 &key,
831 &EndpointRateLimit {
832 window_seconds: 1.0,
833 max_requests: 3.0,
834 },
835 )
836 .await
837 .unwrap()
838 });
839 }
840 let mut admitted = 0;
841 while let Some(result) = tasks.join_next().await {
842 if matches!(result.unwrap(), RateLimitDecision::Allowed) {
843 admitted += 1;
844 }
845 }
846 assert_eq!(admitted, 3);
847 assert_eq!(first.get(&key).await.unwrap().unwrap(), "24");
848 tokio::time::sleep(Duration::from_millis(650)).await;
849 assert!(matches!(
850 storage.first().unwrap().consume(&key, &rule).await.unwrap(),
851 RateLimitDecision::Blocked { retry_after: 1.0 }
852 ));
853 tokio::time::sleep(Duration::from_millis(450)).await;
854 assert!(matches!(
855 storage.last().unwrap().consume(&key, &rule).await.unwrap(),
856 RateLimitDecision::Allowed
857 ));
858 assert_eq!(second.get(&key).await.unwrap().unwrap(), "1");
859 first.delete(&key).await.unwrap();
860 let invalid_key = format!("invalid-{key}");
861 assert!(
862 first
863 .increment(&invalid_key, Duration::from_millis(300))
864 .await
865 .is_err()
866 );
867 assert!(first.get(&invalid_key).await.unwrap().is_none());
868
869 let unavailable = Arc::new(RedisAdapter::new("redis://127.0.0.1:9").await.unwrap());
870 let middleware = RateLimitMiddleware::new(
871 RateLimitConfig::new().storage(Arc::new(CacheRateLimitStorage::new(unavailable))),
872 );
873 let error = middleware
874 .before_request(&make_request("/get-session", "198.51.100.12"))
875 .await
876 .unwrap_err()
877 .to_auth_response();
878 assert_eq!(error.status, 500);
879 assert!(!String::from_utf8_lossy(&error.body).contains("Redis"));
880 }
881}
882