fraiseql_server/middleware/rate_limit/
dispatch.rs1#[cfg(feature = "redis-rate-limiting")]
8use super::redis::RedisRateLimiter;
9use super::{
10 config::{CheckResult, RateLimitConfig, RateLimitingSecurityConfig},
11 in_memory::InMemoryRateLimiter,
12};
13
14#[non_exhaustive]
20pub enum RateLimiter {
21 InMemory(InMemoryRateLimiter),
23 #[cfg(feature = "redis-rate-limiting")]
25 Redis(RedisRateLimiter),
26}
27
28impl RateLimiter {
29 pub fn new(config: RateLimitConfig) -> Self {
31 Self::InMemory(InMemoryRateLimiter::new(config))
32 }
33
34 #[cfg(feature = "redis-rate-limiting")]
41 pub async fn new_redis(url: &str, config: RateLimitConfig) -> Result<Self, redis::RedisError> {
42 let rl = RedisRateLimiter::new(url, config).await?;
43 Ok(Self::Redis(rl))
44 }
45
46 #[must_use]
48 pub fn with_path_rules_from_security(self, sec: &RateLimitingSecurityConfig) -> Self {
49 match self {
50 Self::InMemory(rl) => Self::InMemory(rl.with_path_rules_from_security(sec)),
51 #[cfg(feature = "redis-rate-limiting")]
52 Self::Redis(rl) => Self::Redis(rl.with_path_rules_from_security(sec)),
53 }
54 }
55
56 pub const fn config(&self) -> &RateLimitConfig {
58 match self {
59 Self::InMemory(rl) => rl.config(),
60 #[cfg(feature = "redis-rate-limiting")]
61 Self::Redis(rl) => rl.config(),
62 }
63 }
64
65 pub const fn path_rule_count(&self) -> usize {
67 match self {
68 Self::InMemory(rl) => rl.path_rule_count(),
69 #[cfg(feature = "redis-rate-limiting")]
70 Self::Redis(rl) => rl.path_rule_count(),
71 }
72 }
73
74 pub fn retry_after_for_path(&self, path: &str) -> u32 {
79 match self {
80 Self::InMemory(rl) => rl.retry_after_for_path(path),
81 #[cfg(feature = "redis-rate-limiting")]
82 Self::Redis(rl) => rl.retry_after_for_path(path),
83 }
84 }
85
86 pub async fn check_ip_limit(&self, ip: &str) -> CheckResult {
88 match self {
89 Self::InMemory(rl) => rl.check_ip_limit(ip).await,
90 #[cfg(feature = "redis-rate-limiting")]
91 Self::Redis(rl) => rl.check_ip_limit(ip).await,
92 }
93 }
94
95 pub async fn check_user_limit(&self, user_id: &str) -> CheckResult {
97 match self {
98 Self::InMemory(rl) => rl.check_user_limit(user_id).await,
99 #[cfg(feature = "redis-rate-limiting")]
100 Self::Redis(rl) => rl.check_user_limit(user_id).await,
101 }
102 }
103
104 pub async fn check_path_limit(&self, path: &str, ip: &str) -> CheckResult {
110 match self {
111 Self::InMemory(rl) => rl.check_path_limit(path, ip).await,
112 #[cfg(feature = "redis-rate-limiting")]
113 Self::Redis(rl) => rl.check_path_limit(path, ip).await,
114 }
115 }
116
117 pub async fn cleanup(&self) {
121 match self {
122 Self::InMemory(rl) => rl.cleanup().await,
123 #[cfg(feature = "redis-rate-limiting")]
124 Self::Redis(_) => {},
125 }
126 }
127
128 #[must_use]
135 pub fn retry_after_secs(&self) -> u32 {
136 let rps = self.config().rps_per_ip;
137 if rps == 0 {
138 return 1;
139 }
140 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
141 {
143 ((1.0_f64 / f64::from(rps)).ceil() as u32).max(1)
144 }
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn new_creates_in_memory_backend() {
154 let limiter = RateLimiter::new(RateLimitConfig::default());
155 assert!(matches!(limiter, RateLimiter::InMemory(_)));
156 }
157
158 #[test]
159 fn config_returns_reference_to_inner_config() {
160 let config = RateLimitConfig {
161 rps_per_ip: 42,
162 ..RateLimitConfig::default()
163 };
164 let limiter = RateLimiter::new(config);
165 assert_eq!(limiter.config().rps_per_ip, 42);
166 }
167
168 #[test]
169 fn path_rule_count_starts_at_zero() {
170 let limiter = RateLimiter::new(RateLimitConfig::default());
171 assert_eq!(limiter.path_rule_count(), 0);
172 }
173
174 #[test]
175 fn retry_after_secs_minimum_is_one() {
176 let config = RateLimitConfig {
177 rps_per_ip: u32::MAX,
178 ..RateLimitConfig::default()
179 };
180 let limiter = RateLimiter::new(config);
181 assert_eq!(limiter.retry_after_secs(), 1, "minimum retry_after must be 1s");
182 }
183}