1use crate::config::CacheStore;
17use crate::rate_limit::{RateLimit, RateLimiter};
18use crate::store::Cache;
19use rustlavel_http::handler::BoxFuture;
20use rustlavel_http::{Middleware, Next, Request, Response, Status};
21use rustlavel_core::Json;
22use std::sync::Arc;
23use std::time::Duration;
24
25type KeyFn = Arc<dyn Fn(&Request) -> String + Send + Sync>;
27
28#[derive(Clone)]
30pub struct Throttle {
31 limiter: RateLimiter,
32 max: u64,
33 window: Duration,
34 key: KeyFn,
35}
36
37impl Throttle {
38 pub fn new(cache: &CacheStore, max: u64, window: Duration) -> Self {
40 Throttle {
41 limiter: RateLimiter::new(cache.driver_handle()),
42 max,
43 window,
44 key: Arc::new(default_key),
45 }
46 }
47
48 pub fn per_minute(cache: &CacheStore, max: u64) -> Self {
50 Throttle::new(cache, max, Duration::from_secs(60))
51 }
52
53 pub fn per_second(cache: &CacheStore, max: u64) -> Self {
54 Throttle::new(cache, max, Duration::from_secs(1))
55 }
56
57 pub fn with_driver(store: Arc<dyn Cache>, max: u64, window: Duration) -> Self {
60 Throttle { limiter: RateLimiter::new(store), max, window, key: Arc::new(default_key) }
61 }
62
63 pub fn by(mut self, key: impl Fn(&Request) -> String + Send + Sync + 'static) -> Self {
69 self.key = Arc::new(key);
70 self
71 }
72
73 fn headers(response: Response, outcome: &RateLimit) -> Response {
74 response
75 .with_header("x-ratelimit-limit", outcome.limit.to_string())
76 .with_header("x-ratelimit-remaining", outcome.remaining.to_string())
77 }
78}
79
80fn default_key(request: &Request) -> String {
86 let who = request.ip().unwrap_or_else(|| "unknown".to_string());
87 let what = request.route().unwrap_or_else(|| request.path());
88 format!("{who}|{what}")
89}
90
91impl Middleware for Throttle {
92 fn handle(&self, request: Request, next: Next) -> BoxFuture<Response> {
93 let limiter = self.limiter.clone();
94 let max = self.max;
95 let window = self.window;
96 let key = (self.key)(&request);
97
98 Box::pin(async move {
99 let outcome = match limiter.attempt(&key, max, window).await {
100 Ok(outcome) => outcome,
101 Err(_) => return next.run(request).await,
104 };
105
106 if outcome.exceeded {
107 let retry_after = outcome.retry_after_seconds();
108 let body = Json::object([
109 ("message", Json::from("Too many requests.")),
110 ("retry_after", Json::from(retry_after)),
111 ]);
112
113 let response = Response::new(Status::TOO_MANY_REQUESTS)
114 .with_json(body)
115 .with_header("retry-after", retry_after.to_string())
116 .with_header("x-ratelimit-reset", outcome.reset_at().to_string());
117 return Throttle::headers(response, &outcome);
118 }
119
120 Throttle::headers(next.run(request).await, &outcome)
121 })
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::memory::MemoryStore;
129 use rustlavel_http::{Method, Router, TestClient};
130
131 fn client(throttle: Throttle) -> TestClient {
132 let mut router = Router::new();
133 router.get("/api/search", |_req: Request| async { "results" });
134 router.get("/api/health", |_req: Request| async { "ok" });
135 router.middleware(throttle);
136 TestClient::new(router)
137 }
138
139 fn store() -> Arc<dyn Cache> {
140 Arc::new(MemoryStore::new())
141 }
142
143 fn from(ip: &str, path: &str) -> Request {
146 Request::new(Method::Get, path).with_header("x-forwarded-for", ip)
147 }
148
149 #[tokio::test]
150 async fn the_first_requests_pass_and_carry_the_rate_limit_headers() {
151 let client = client(Throttle::with_driver(store(), 3, Duration::from_secs(60)));
152
153 for expected_remaining in ["2", "1", "0"] {
154 client
155 .send(from("10.0.0.1", "/api/search"))
156 .await
157 .assert_ok()
158 .assert_see("results")
159 .assert_header("x-ratelimit-limit", "3")
160 .assert_header("x-ratelimit-remaining", expected_remaining);
161 }
162 }
163
164 #[tokio::test]
165 async fn the_request_after_the_limit_is_refused_with_429_and_retry_after() {
166 let client = client(Throttle::with_driver(store(), 2, Duration::from_secs(60)));
167
168 client.send(from("10.0.0.2", "/api/search")).await.assert_ok();
169 client.send(from("10.0.0.2", "/api/search")).await.assert_ok();
170
171 let refused = client
172 .send(from("10.0.0.2", "/api/search"))
173 .await
174 .assert_status(429)
175 .assert_header("x-ratelimit-limit", "2")
176 .assert_header("x-ratelimit-remaining", "0")
177 .assert_json("message", "Too many requests.");
178
179 let retry_after: u64 =
180 refused.header("retry-after").expect("a 429 must say when to come back").parse().unwrap();
181 assert!((1..=60).contains(&retry_after), "retry-after was {retry_after}");
182 assert!(refused.header("x-ratelimit-reset").is_some());
183 }
184
185 #[tokio::test]
186 async fn two_client_addresses_get_their_own_allowance() {
187 let client = client(Throttle::with_driver(store(), 1, Duration::from_secs(60)));
188
189 client.send(from("10.0.0.3", "/api/search")).await.assert_ok();
190 client.send(from("10.0.0.3", "/api/search")).await.assert_status(429);
191
192 client.send(from("10.0.0.4", "/api/search")).await.assert_ok();
194 }
195
196 #[tokio::test]
197 async fn two_routes_get_their_own_allowance() {
198 let client = client(Throttle::with_driver(store(), 1, Duration::from_secs(60)));
199
200 client.send(from("10.0.0.5", "/api/search")).await.assert_ok();
201 client.send(from("10.0.0.5", "/api/search")).await.assert_status(429);
202
203 client.send(from("10.0.0.5", "/api/health")).await.assert_ok();
204 }
205
206 #[tokio::test]
207 async fn a_custom_key_function_replaces_the_ip() {
208 let throttle = Throttle::with_driver(store(), 1, Duration::from_secs(60))
209 .by(|request: &Request| request.header("x-api-key").unwrap_or("anonymous").to_string());
210
211 let client = client(throttle);
212
213 let with_token = |token: &str| {
214 Request::new(Method::Get, "/api/search")
215 .with_header("x-forwarded-for", "10.0.0.6")
216 .with_header("x-api-key", token)
217 };
218
219 client.send(with_token("alpha")).await.assert_ok();
220 client.send(with_token("alpha")).await.assert_status(429);
221 client.send(with_token("beta")).await.assert_ok();
223 }
224
225 #[tokio::test]
226 async fn the_allowance_comes_back_when_the_window_passes() {
227 let window = Duration::from_millis(200);
232 let client = client(Throttle::with_driver(store(), 1, window));
233
234 client.send(from("10.0.0.7", "/api/search")).await.assert_ok();
235
236 tokio::time::sleep(window * 3).await;
237 client.send(from("10.0.0.7", "/api/search")).await.assert_ok();
238 }
239
240 #[tokio::test]
241 async fn the_second_request_inside_the_window_is_refused() {
242 let client = client(Throttle::with_driver(store(), 1, Duration::from_secs(60)));
245
246 client.send(from("10.0.0.8", "/api/search")).await.assert_ok();
247 client.send(from("10.0.0.8", "/api/search")).await.assert_status(429);
248 }
249
250 #[tokio::test]
251 async fn the_handler_never_runs_once_the_limit_is_reached() {
252 let mut router = Router::new();
253 router.get("/once", |_req: Request| async {
254 static SEEN: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
257 let count = SEEN.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
258 assert_eq!(count, 0, "the handler ran after the limit was reached");
259 "ok"
260 });
261 router.middleware(Throttle::with_driver(store(), 1, Duration::from_secs(60)));
262
263 let client = TestClient::new(router);
264 client.send(from("10.0.0.8", "/once")).await.assert_ok();
265 client.send(from("10.0.0.8", "/once")).await.assert_status(429);
266 }
267
268 #[tokio::test]
269 async fn a_request_without_an_ip_still_falls_under_a_limit() {
270 let client = client(Throttle::with_driver(store(), 1, Duration::from_secs(60)));
271
272 client.send(Request::new(Method::Get, "/api/search")).await.assert_ok();
274 client.send(Request::new(Method::Get, "/api/search")).await.assert_status(429);
275 }
276
277 #[tokio::test]
278 async fn a_throttle_built_from_a_cache_store_works_the_same_way() {
279 let store = CacheStore::from_driver(MemoryStore::new());
280 let client = client(Throttle::per_minute(&store, 1));
281
282 client.send(from("10.0.0.9", "/api/search")).await.assert_ok();
283 client.send(from("10.0.0.9", "/api/search")).await.assert_status(429);
284 }
285}