use std::future::{ready, Ready};
use std::num::NonZeroU32;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
use actix_web::body::{EitherBody, MessageBody};
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
use actix_web::http::StatusCode;
use actix_web::{Error, HttpResponse, ResponseError};
use futures::future::LocalBoxFuture;
use governor::clock::{Clock, DefaultClock};
use governor::state::keyed::DefaultKeyedStateStore;
use governor::{Quota, RateLimiter as GovernorRateLimiter};
pub trait KeyExtractor: Clone + 'static {
type Key: Clone + Eq + std::hash::Hash + Send + Sync + 'static;
type KeyExtractionError: ResponseError + 'static;
fn extract(&self, req: &ServiceRequest) -> Result<Self::Key, Self::KeyExtractionError>;
}
#[derive(Debug, Clone, Copy)]
pub struct SimpleKeyExtractionError(pub &'static str);
impl SimpleKeyExtractionError {
pub const fn new(body: &'static str) -> Self {
Self(body)
}
}
impl std::fmt::Display for SimpleKeyExtractionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SimpleKeyExtractionError")
}
}
impl std::error::Error for SimpleKeyExtractionError {}
impl ResponseError for SimpleKeyExtractionError {
fn status_code(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
fn error_response(&self) -> HttpResponse {
crate::error::json_error(self.status_code(), self.0)
}
}
type KeyedLimiter<Key> = GovernorRateLimiter<Key, DefaultKeyedStateStore<Key>, DefaultClock>;
pub struct RateLimiterConfig<K: KeyExtractor> {
limiter: Arc<KeyedLimiter<K::Key>>,
key_extractor: K,
}
impl<K: KeyExtractor> Clone for RateLimiterConfig<K> {
fn clone(&self) -> Self {
Self {
limiter: self.limiter.clone(),
key_extractor: self.key_extractor.clone(),
}
}
}
impl<K: KeyExtractor> RateLimiterConfig<K> {
pub fn new(period: Duration, burst_size: u32, key_extractor: K) -> Self {
let burst = NonZeroU32::new(burst_size).expect("burst_size must be non-zero");
let quota = Quota::with_period(period)
.expect("period must be non-zero")
.allow_burst(burst);
Self {
limiter: Arc::new(GovernorRateLimiter::keyed(quota)),
key_extractor,
}
}
}
pub struct RateLimit<K: KeyExtractor> {
limiter: Arc<KeyedLimiter<K::Key>>,
key_extractor: K,
}
impl<K: KeyExtractor> RateLimit<K> {
pub fn new(config: &RateLimiterConfig<K>) -> Self {
Self {
limiter: config.limiter.clone(),
key_extractor: config.key_extractor.clone(),
}
}
}
impl<S, B, K> Transform<S, ServiceRequest> for RateLimit<K>
where
K: KeyExtractor,
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Transform = RateLimitMiddleware<S, K>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(RateLimitMiddleware {
service: Rc::new(service),
limiter: self.limiter.clone(),
key_extractor: self.key_extractor.clone(),
}))
}
}
pub struct RateLimitMiddleware<S, K: KeyExtractor> {
service: Rc<S>,
limiter: Arc<KeyedLimiter<K::Key>>,
key_extractor: K,
}
impl<S, B, K> Service<ServiceRequest> for RateLimitMiddleware<S, K>
where
K: KeyExtractor,
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let key = match self.key_extractor.extract(&req) {
Ok(key) => key,
Err(err) => {
return Box::pin(async move { Err(err.into()) });
}
};
match self.limiter.check_key(&key) {
Ok(_) => {
let fut = self.service.call(req);
Box::pin(async move { fut.await.map(|res| res.map_into_left_body()) })
}
Err(negative) => {
let wait_time = negative
.wait_time_from(DefaultClock::default().now())
.as_secs();
let mut response = crate::error::json_error(
StatusCode::TOO_MANY_REQUESTS,
format!("Too many requests, retry in {wait_time}s"),
);
response.headers_mut().insert(
actix_web::http::header::RETRY_AFTER,
actix_web::http::header::HeaderValue::from(wait_time),
);
response.headers_mut().insert(
actix_web::http::header::HeaderName::from_static("x-ratelimit-after"),
actix_web::http::header::HeaderValue::from(wait_time),
);
let response = req.into_response(response).map_into_right_body();
Box::pin(async move { Ok(response) })
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::http::StatusCode as HttpStatusCode;
use actix_web::{test, web, App, HttpResponse as Resp};
use std::net::{IpAddr, Ipv4Addr};
#[derive(Clone)]
struct FixedKeyExtractor(IpAddr);
impl KeyExtractor for FixedKeyExtractor {
type Key = IpAddr;
type KeyExtractionError = SimpleKeyExtractionError;
fn extract(&self, _req: &ServiceRequest) -> Result<Self::Key, Self::KeyExtractionError> {
Ok(self.0)
}
}
#[actix_web::test]
async fn burst_then_429_with_retry_after_header() {
let key = IpAddr::V4(Ipv4Addr::new(10, 1, 1, 1));
let conf = RateLimiterConfig::new(Duration::from_millis(100), 3, FixedKeyExtractor(key));
let app = test::init_service(
App::new()
.wrap(RateLimit::new(&conf))
.route("/", web::get().to(|| async { Resp::Ok().finish() })),
)
.await;
for _ in 0..3 {
let res =
test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
assert_eq!(res.status(), HttpStatusCode::OK);
}
let res = test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
assert_eq!(res.status(), HttpStatusCode::TOO_MANY_REQUESTS);
assert!(
res.headers().contains_key("retry-after"),
"a 429 must carry Retry-After"
);
assert!(
res.headers().contains_key("x-ratelimit-after"),
"a 429 must carry x-ratelimit-after"
);
let body: serde_json::Value = test::read_body_json(res).await;
assert_eq!(body["error"]["type"], "api_error");
assert!(body["error"]["message"]
.as_str()
.is_some_and(|message| message.starts_with("Too many requests, retry in ")));
}
#[actix_web::test]
async fn refills_after_period_elapses() {
let key = IpAddr::V4(Ipv4Addr::new(10, 1, 1, 2));
let conf = RateLimiterConfig::new(Duration::from_millis(20), 1, FixedKeyExtractor(key));
let app = test::init_service(
App::new()
.wrap(RateLimit::new(&conf))
.route("/", web::get().to(|| async { Resp::Ok().finish() })),
)
.await;
let res = test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
assert_eq!(res.status(), HttpStatusCode::OK);
let res = test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
assert_eq!(
res.status(),
HttpStatusCode::TOO_MANY_REQUESTS,
"bucket must be empty immediately after the burst"
);
tokio::time::sleep(Duration::from_millis(60)).await;
let res = test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
assert_eq!(
res.status(),
HttpStatusCode::OK,
"the bucket must have refilled after the period elapses"
);
}
#[actix_web::test]
async fn extraction_failure_returns_extractors_error_response() {
#[derive(Clone)]
struct AlwaysFailsExtractor;
impl KeyExtractor for AlwaysFailsExtractor {
type Key = ();
type KeyExtractionError = SimpleKeyExtractionError;
fn extract(
&self,
_req: &ServiceRequest,
) -> Result<Self::Key, Self::KeyExtractionError> {
Err(SimpleKeyExtractionError::new("no key for you"))
}
}
let conf = RateLimiterConfig::new(Duration::from_secs(1), 1, AlwaysFailsExtractor);
let app = test::init_service(
App::new()
.wrap(RateLimit::new(&conf))
.route("/", web::get().to(|| async { Resp::Ok().finish() })),
)
.await;
let err = test::try_call_service(&app, test::TestRequest::get().uri("/").to_request())
.await
.expect_err("extraction failure must reach the caller as an Err");
let response = err.error_response();
assert_eq!(response.status(), HttpStatusCode::INTERNAL_SERVER_ERROR);
let body = actix_web::body::to_bytes(response.into_body())
.await
.unwrap();
let body: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(body["error"]["message"], "no key for you");
assert_eq!(body["error"]["type"], "api_error");
}
}