1use std::future::{ready, Ready};
28use std::num::NonZeroU32;
29use std::rc::Rc;
30use std::sync::Arc;
31use std::time::Duration;
32
33use actix_web::body::{EitherBody, MessageBody};
34use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
35use actix_web::http::StatusCode;
36use actix_web::{Error, HttpResponse, HttpResponseBuilder, ResponseError};
37use futures::future::LocalBoxFuture;
38use governor::clock::{Clock, DefaultClock};
39use governor::state::keyed::DefaultKeyedStateStore;
40use governor::{Quota, RateLimiter as GovernorRateLimiter};
41
42pub trait KeyExtractor: Clone + 'static {
49 type Key: Clone + Eq + std::hash::Hash + Send + Sync + 'static;
51 type KeyExtractionError: ResponseError + 'static;
54
55 fn extract(&self, req: &ServiceRequest) -> Result<Self::Key, Self::KeyExtractionError>;
57}
58
59#[derive(Debug, Clone, Copy)]
65pub struct SimpleKeyExtractionError(pub &'static str);
66
67impl SimpleKeyExtractionError {
68 pub const fn new(body: &'static str) -> Self {
69 Self(body)
70 }
71}
72
73impl std::fmt::Display for SimpleKeyExtractionError {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 write!(f, "SimpleKeyExtractionError")
76 }
77}
78
79impl std::error::Error for SimpleKeyExtractionError {}
80
81impl ResponseError for SimpleKeyExtractionError {
82 fn status_code(&self) -> StatusCode {
83 StatusCode::INTERNAL_SERVER_ERROR
84 }
85
86 fn error_response(&self) -> HttpResponse {
87 HttpResponseBuilder::new(self.status_code())
88 .content_type("text/plain; charset=utf-8")
89 .body(self.0)
90 }
91}
92
93type KeyedLimiter<Key> = GovernorRateLimiter<Key, DefaultKeyedStateStore<Key>, DefaultClock>;
94
95pub struct RateLimiterConfig<K: KeyExtractor> {
100 limiter: Arc<KeyedLimiter<K::Key>>,
101 key_extractor: K,
102}
103
104impl<K: KeyExtractor> Clone for RateLimiterConfig<K> {
105 fn clone(&self) -> Self {
106 Self {
107 limiter: self.limiter.clone(),
108 key_extractor: self.key_extractor.clone(),
109 }
110 }
111}
112
113impl<K: KeyExtractor> RateLimiterConfig<K> {
114 pub fn new(period: Duration, burst_size: u32, key_extractor: K) -> Self {
123 let burst = NonZeroU32::new(burst_size).expect("burst_size must be non-zero");
124 let quota = Quota::with_period(period)
125 .expect("period must be non-zero")
126 .allow_burst(burst);
127 Self {
128 limiter: Arc::new(GovernorRateLimiter::keyed(quota)),
129 key_extractor,
130 }
131 }
132}
133
134pub struct RateLimit<K: KeyExtractor> {
136 limiter: Arc<KeyedLimiter<K::Key>>,
137 key_extractor: K,
138}
139
140impl<K: KeyExtractor> RateLimit<K> {
141 pub fn new(config: &RateLimiterConfig<K>) -> Self {
143 Self {
144 limiter: config.limiter.clone(),
145 key_extractor: config.key_extractor.clone(),
146 }
147 }
148}
149
150impl<S, B, K> Transform<S, ServiceRequest> for RateLimit<K>
151where
152 K: KeyExtractor,
153 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
154 B: MessageBody + 'static,
155{
156 type Response = ServiceResponse<EitherBody<B>>;
157 type Error = Error;
158 type Transform = RateLimitMiddleware<S, K>;
159 type InitError = ();
160 type Future = Ready<Result<Self::Transform, Self::InitError>>;
161
162 fn new_transform(&self, service: S) -> Self::Future {
163 ready(Ok(RateLimitMiddleware {
164 service: Rc::new(service),
165 limiter: self.limiter.clone(),
166 key_extractor: self.key_extractor.clone(),
167 }))
168 }
169}
170
171pub struct RateLimitMiddleware<S, K: KeyExtractor> {
172 service: Rc<S>,
173 limiter: Arc<KeyedLimiter<K::Key>>,
174 key_extractor: K,
175}
176
177impl<S, B, K> Service<ServiceRequest> for RateLimitMiddleware<S, K>
178where
179 K: KeyExtractor,
180 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
181 B: MessageBody + 'static,
182{
183 type Response = ServiceResponse<EitherBody<B>>;
184 type Error = Error;
185 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
186
187 forward_ready!(service);
188
189 fn call(&self, req: ServiceRequest) -> Self::Future {
190 let key = match self.key_extractor.extract(&req) {
191 Ok(key) => key,
192 Err(err) => {
193 return Box::pin(async move { Err(err.into()) });
197 }
198 };
199
200 match self.limiter.check_key(&key) {
201 Ok(_) => {
202 let fut = self.service.call(req);
203 Box::pin(async move { fut.await.map(|res| res.map_into_left_body()) })
204 }
205 Err(negative) => {
206 let wait_time = negative
207 .wait_time_from(DefaultClock::default().now())
208 .as_secs();
209 let response = HttpResponse::build(StatusCode::TOO_MANY_REQUESTS)
210 .insert_header(("retry-after", wait_time))
211 .insert_header(("x-ratelimit-after", wait_time))
212 .content_type("text/plain; charset=utf-8")
213 .body(format!("Too many requests, retry in {wait_time}s"));
214 let response = req.into_response(response).map_into_right_body();
215 Box::pin(async move { Ok(response) })
216 }
217 }
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use actix_web::http::StatusCode as HttpStatusCode;
225 use actix_web::{test, web, App, HttpResponse as Resp};
226 use std::net::{IpAddr, Ipv4Addr};
227
228 #[derive(Clone)]
233 struct FixedKeyExtractor(IpAddr);
234
235 impl KeyExtractor for FixedKeyExtractor {
236 type Key = IpAddr;
237 type KeyExtractionError = SimpleKeyExtractionError;
238
239 fn extract(&self, _req: &ServiceRequest) -> Result<Self::Key, Self::KeyExtractionError> {
240 Ok(self.0)
241 }
242 }
243
244 #[actix_web::test]
245 async fn burst_then_429_with_retry_after_header() {
246 let key = IpAddr::V4(Ipv4Addr::new(10, 1, 1, 1));
247 let conf = RateLimiterConfig::new(Duration::from_millis(100), 3, FixedKeyExtractor(key));
248 let app = test::init_service(
249 App::new()
250 .wrap(RateLimit::new(&conf))
251 .route("/", web::get().to(|| async { Resp::Ok().finish() })),
252 )
253 .await;
254
255 for _ in 0..3 {
257 let res =
258 test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
259 assert_eq!(res.status(), HttpStatusCode::OK);
260 }
261
262 let res = test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
264 assert_eq!(res.status(), HttpStatusCode::TOO_MANY_REQUESTS);
265 assert!(
266 res.headers().contains_key("retry-after"),
267 "a 429 must carry Retry-After"
268 );
269 assert!(
270 res.headers().contains_key("x-ratelimit-after"),
271 "a 429 must carry x-ratelimit-after"
272 );
273 }
274
275 #[actix_web::test]
276 async fn refills_after_period_elapses() {
277 let key = IpAddr::V4(Ipv4Addr::new(10, 1, 1, 2));
278 let conf = RateLimiterConfig::new(Duration::from_millis(20), 1, FixedKeyExtractor(key));
280 let app = test::init_service(
281 App::new()
282 .wrap(RateLimit::new(&conf))
283 .route("/", web::get().to(|| async { Resp::Ok().finish() })),
284 )
285 .await;
286
287 let res = test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
288 assert_eq!(res.status(), HttpStatusCode::OK);
289
290 let res = test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
291 assert_eq!(
292 res.status(),
293 HttpStatusCode::TOO_MANY_REQUESTS,
294 "bucket must be empty immediately after the burst"
295 );
296
297 tokio::time::sleep(Duration::from_millis(60)).await;
298
299 let res = test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
300 assert_eq!(
301 res.status(),
302 HttpStatusCode::OK,
303 "the bucket must have refilled after the period elapses"
304 );
305 }
306
307 #[actix_web::test]
308 async fn extraction_failure_returns_extractors_error_response() {
309 #[derive(Clone)]
310 struct AlwaysFailsExtractor;
311
312 impl KeyExtractor for AlwaysFailsExtractor {
313 type Key = ();
314 type KeyExtractionError = SimpleKeyExtractionError;
315
316 fn extract(
317 &self,
318 _req: &ServiceRequest,
319 ) -> Result<Self::Key, Self::KeyExtractionError> {
320 Err(SimpleKeyExtractionError::new("no key for you"))
321 }
322 }
323
324 let conf = RateLimiterConfig::new(Duration::from_secs(1), 1, AlwaysFailsExtractor);
325 let app = test::init_service(
326 App::new()
327 .wrap(RateLimit::new(&conf))
328 .route("/", web::get().to(|| async { Resp::Ok().finish() })),
329 )
330 .await;
331
332 let err = test::try_call_service(&app, test::TestRequest::get().uri("/").to_request())
339 .await
340 .expect_err("extraction failure must reach the caller as an Err");
341 let response = err.error_response();
342 assert_eq!(response.status(), HttpStatusCode::INTERNAL_SERVER_ERROR);
343 }
344}