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 {
150 Request::new(Method::Get, path).with_peer(format!("{ip}:44321").parse().expect("an address"))
151 }
152
153 #[tokio::test]
154 async fn the_first_requests_pass_and_carry_the_rate_limit_headers() {
155 let client = client(Throttle::with_driver(store(), 3, Duration::from_secs(60)));
156
157 for expected_remaining in ["2", "1", "0"] {
158 client
159 .send(from("10.0.0.1", "/api/search"))
160 .await
161 .assert_ok()
162 .assert_see("results")
163 .assert_header("x-ratelimit-limit", "3")
164 .assert_header("x-ratelimit-remaining", expected_remaining);
165 }
166 }
167
168 #[tokio::test]
169 async fn the_request_after_the_limit_is_refused_with_429_and_retry_after() {
170 let client = client(Throttle::with_driver(store(), 2, Duration::from_secs(60)));
171
172 client.send(from("10.0.0.2", "/api/search")).await.assert_ok();
173 client.send(from("10.0.0.2", "/api/search")).await.assert_ok();
174
175 let refused = client
176 .send(from("10.0.0.2", "/api/search"))
177 .await
178 .assert_status(429)
179 .assert_header("x-ratelimit-limit", "2")
180 .assert_header("x-ratelimit-remaining", "0")
181 .assert_json("message", "Too many requests.");
182
183 let retry_after: u64 =
184 refused.header("retry-after").expect("a 429 must say when to come back").parse().unwrap();
185 assert!((1..=60).contains(&retry_after), "retry-after was {retry_after}");
186 assert!(refused.header("x-ratelimit-reset").is_some());
187 }
188
189 #[tokio::test]
190 async fn two_client_addresses_get_their_own_allowance() {
191 let client = client(Throttle::with_driver(store(), 1, Duration::from_secs(60)));
192
193 client.send(from("10.0.0.3", "/api/search")).await.assert_ok();
194 client.send(from("10.0.0.3", "/api/search")).await.assert_status(429);
195
196 client.send(from("10.0.0.4", "/api/search")).await.assert_ok();
198 }
199
200 #[tokio::test]
201 async fn two_routes_get_their_own_allowance() {
202 let client = client(Throttle::with_driver(store(), 1, Duration::from_secs(60)));
203
204 client.send(from("10.0.0.5", "/api/search")).await.assert_ok();
205 client.send(from("10.0.0.5", "/api/search")).await.assert_status(429);
206
207 client.send(from("10.0.0.5", "/api/health")).await.assert_ok();
208 }
209
210 #[tokio::test]
211 async fn a_custom_key_function_replaces_the_ip() {
212 let throttle = Throttle::with_driver(store(), 1, Duration::from_secs(60))
213 .by(|request: &Request| request.header("x-api-key").unwrap_or("anonymous").to_string());
214
215 let client = client(throttle);
216
217 let with_token = |token: &str| {
218 Request::new(Method::Get, "/api/search")
219 .with_peer("10.0.0.6:44321".parse().expect("an address"))
220 .with_header("x-api-key", token)
221 };
222
223 client.send(with_token("alpha")).await.assert_ok();
224 client.send(with_token("alpha")).await.assert_status(429);
225 client.send(with_token("beta")).await.assert_ok();
227 }
228
229 #[tokio::test]
230 async fn the_allowance_comes_back_when_the_window_passes() {
231 let window = Duration::from_millis(200);
236 let client = client(Throttle::with_driver(store(), 1, window));
237
238 client.send(from("10.0.0.7", "/api/search")).await.assert_ok();
239
240 tokio::time::sleep(window * 3).await;
241 client.send(from("10.0.0.7", "/api/search")).await.assert_ok();
242 }
243
244 #[tokio::test]
245 async fn the_second_request_inside_the_window_is_refused() {
246 let client = client(Throttle::with_driver(store(), 1, Duration::from_secs(60)));
249
250 client.send(from("10.0.0.8", "/api/search")).await.assert_ok();
251 client.send(from("10.0.0.8", "/api/search")).await.assert_status(429);
252 }
253
254 #[tokio::test]
255 async fn the_handler_never_runs_once_the_limit_is_reached() {
256 let mut router = Router::new();
257 router.get("/once", |_req: Request| async {
258 static SEEN: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
261 let count = SEEN.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
262 assert_eq!(count, 0, "the handler ran after the limit was reached");
263 "ok"
264 });
265 router.middleware(Throttle::with_driver(store(), 1, Duration::from_secs(60)));
266
267 let client = TestClient::new(router);
268 client.send(from("10.0.0.8", "/once")).await.assert_ok();
269 client.send(from("10.0.0.8", "/once")).await.assert_status(429);
270 }
271
272 #[tokio::test]
273 async fn a_request_without_an_ip_still_falls_under_a_limit() {
274 let client = client(Throttle::with_driver(store(), 1, Duration::from_secs(60)));
275
276 client.send(Request::new(Method::Get, "/api/search")).await.assert_ok();
278 client.send(Request::new(Method::Get, "/api/search")).await.assert_status(429);
279 }
280
281 #[tokio::test]
282 async fn a_throttle_built_from_a_cache_store_works_the_same_way() {
283 let store = CacheStore::from_driver(MemoryStore::new());
284 let client = client(Throttle::per_minute(&store, 1));
285
286 client.send(from("10.0.0.9", "/api/search")).await.assert_ok();
287 client.send(from("10.0.0.9", "/api/search")).await.assert_status(429);
288 }
289}