Skip to main content

actix_web_ratelimit/
lib.rs

1/*!
2A simple and highly customizable rate limiting middleware for actix-web 4.
3
4For complete documentation and examples, visit the [GitHub repository](https://github.com/bigyao25/actix-web-ratelimit).
5
6## Features
7
8- **actix-web 4 Compatible**: Built specifically for actix-web 4
9- **Simple & Easy to Use**: Minimal configuration required
10- **Pluggable Storage**: Support for in-memory and Redis storage backends
11- **High Performance**: Efficient sliding window algorithm
12- **Customizable**: Custom client identification and rate limit exceeded handlers
13- **Thread Safe**: Concurrent request handling with DashMap
14
15
16## Quick Start
17
18Add this to your `Cargo.toml`:
19
20```toml
21[dependencies]
22actix-web-ratelimit = "0.1"
23
24# Or, for Redis support
25actix-web-ratelimit = { version = "0.1", features = ["redis"] }
26```
27
28## Usage
29
30### Basic Usage with In-Memory Store
31
32```rust, no_run
33# use actix_web::{App, HttpServer, Responder, web};
34# use actix_web_ratelimit::{RateLimit, config::RateLimitConfig, store::MemoryStore};
35# use std::sync::Arc;
36#
37# async fn index() -> impl Responder {
38#     "Hello world!"
39# }
40#
41# #[actix_web::main]
42# async fn main() -> std::io::Result<()> {
43    // Configure rate limiting: allow 3 requests per 10-second window
44    let config = RateLimitConfig::default().max_requests(3).window_secs(10);
45    // Create in-memory store for tracking request timestamps
46    let store = Arc::new(MemoryStore::new());
47
48    HttpServer::new(move || {
49        App::new()
50            // Create and register the rate limit middleware.
51            .wrap(RateLimit::new(config.clone(), store.clone()))
52            .route("/", web::get().to(index))
53    })
54    .bind(("127.0.0.1", 8080))?
55    .run()
56    .await
57# }
58```
59
60### Advanced Configuration
61
62```rust, no_run
63# use actix_web::HttpResponse;
64# use actix_web::{App, HttpServer, Responder, web};
65# use actix_web_ratelimit::config::RateLimitConfig;
66# use actix_web_ratelimit::{RateLimit, store::MemoryStore};
67# use std::sync::Arc;
68#
69# async fn index() -> impl Responder {
70#     "Hello world!"
71# }
72#
73# #[actix_web::main]
74# async fn main() -> std::io::Result<()> {
75    let store = Arc::new(MemoryStore::new());
76    let config = RateLimitConfig::default()
77        .max_requests(3)
78        .window_secs(10)
79        // Extract client identifier from req. It is IP (realip_remote_addr) by default.
80        .id(|req| {
81            req.headers()
82                .get("X-Client-Id")
83                .and_then(|h| h.to_str().ok())
84                .unwrap_or("anonymous")
85                .to_string()
86        })
87        // Custom handler for rate limit exceeded. It returns a 429 response by default.
88        .exceeded(|id, config, _req| {
89            HttpResponse::TooManyRequests().body(format!(
90                "429 caused: client-id: {}, limit: {}req/{:?}",
91                id, config.max_requests, config.window_secs
92            ))
93        });
94
95    HttpServer::new(move || {
96        App::new()
97            .wrap(RateLimit::new(config.clone(), store.clone()))
98            .route("/", web::get().to(index))
99    })
100    .bind(("127.0.0.1", 8080))?
101    .run()
102    .await
103# }
104```
105
106### Redis Store
107
108First, enable the `redis` feature:
109
110```toml
111[dependencies]
112actix-web-ratelimit = { version = "0.1", features = ["redis"] }
113```
114
115Then you can use Redis as the storage backend:
116
117```rust, no_run
118# #[cfg(feature = "redis")]
119# use actix_web::{App, HttpServer, Responder, web};
120# #[cfg(feature = "redis")]
121# use actix_web_ratelimit::store::RedisStore;
122# #[cfg(feature = "redis")]
123# use actix_web_ratelimit::{RateLimit, config::RateLimitConfig};
124# #[cfg(feature = "redis")]
125# use std::sync::Arc;
126#
127# #[cfg(feature = "redis")]
128# async fn index() -> impl Responder {
129#     "Hello world!"
130# }
131#
132# #[cfg(feature = "redis")]
133# #[actix_web::main]
134# async fn main() -> std::io::Result<()> {
135    let store = Arc::new(
136        RedisStore::new("redis://127.0.0.1/0")
137            .expect("Failed to connect to Redis")
138            // Custom prefix for Redis keys
139            .with_prefix("myapp:ratelimit:"),
140    );
141    let config = RateLimitConfig::default().max_requests(3).window_secs(10);
142
143    HttpServer::new(move || {
144        App::new()
145            .wrap(RateLimit::new(config.clone(), store.clone()))
146            .route("/", web::get().to(index))
147    })
148    .bind(("127.0.0.1", 8080))?
149    .run()
150    .await
151# }
152
153# #[cfg(not(feature = "redis"))]
154# fn main() {}
155
156```
157
158## Storage Backends
159
160This crate provides two built-in storage implementations:
161
162- [`store::MemoryStore`] - In-memory storage using [`dashmap::DashMap`]
163- [`store::RedisStore`] - Distributed storage using Redis (requires `redis` feature)
164
165For custom storage backends, implement the [`store::RateLimitStore`] trait.
166
167## Configuration
168
169Rate limiting behavior is controlled by [`config::RateLimitConfig`]:
170
171- `max_requests` - Maximum requests allowed within the time window
172- `window_secs` - Duration of the sliding time window in seconds
173- `get_id` - Function to extract client identifier from requests
174- `on_exceed` - Function called when rate limit is exceeded
175
176## Related Resources
177
178- [Crates.io](https://crates.io/crates/actix-web-ratelimit) - Package information
179- [Docs.rs](https://docs.rs/actix-web-ratelimit) - API documentation
180- [GitHub](https://github.com/bigyao25/actix-web-ratelimit) - Source code and issues
181- [Examples](https://github.com/bigyao25/actix-web-ratelimit/tree/main/examples) - Usage examples
182
183 */
184pub mod config;
185pub mod store;
186
187use actix_service::{Service, Transform};
188use actix_web::{
189    Error,
190    body::EitherBody,
191    dev::{ServiceRequest, ServiceResponse},
192};
193use futures_util::future::{LocalBoxFuture, Ready, ok};
194use std::{
195    sync::Arc,
196    task::{Context, Poll},
197};
198
199use crate::{config::RateLimitConfig, store::RateLimitStore};
200
201pub struct RateLimit<S>
202where
203    S: RateLimitStore,
204{
205    store: Arc<S>,
206    config: Arc<RateLimitConfig>,
207}
208
209impl<S> RateLimit<S>
210where
211    S: RateLimitStore,
212{
213    pub fn new(config: RateLimitConfig, store: S) -> Self {
214        Self {
215            store: Arc::new(store),
216            config: Arc::new(config),
217        }
218    }
219}
220
221impl<S, B, ST> Transform<S, ServiceRequest> for RateLimit<ST>
222where
223    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
224    S::Future: 'static,
225    B: 'static,
226    ST: RateLimitStore + 'static,
227{
228    type Response = ServiceResponse<EitherBody<B>>;
229    type Error = Error;
230    type Transform = RateLimitMiddleware<S>;
231    type InitError = ();
232    type Future = Ready<Result<Self::Transform, Self::InitError>>;
233
234    fn new_transform(&self, service: S) -> Self::Future {
235        ok(RateLimitMiddleware {
236            service,
237            store: self.store.clone(),
238            config: self.config.clone(),
239        })
240    }
241}
242
243pub struct RateLimitMiddleware<S> {
244    service: S,
245    store: Arc<dyn RateLimitStore>,
246    config: Arc<RateLimitConfig>,
247}
248
249impl<S, B> Service<ServiceRequest> for RateLimitMiddleware<S>
250where
251    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
252    S::Future: 'static,
253    B: 'static,
254{
255    type Response = ServiceResponse<EitherBody<B>>;
256    type Error = Error;
257    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
258
259    fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
260        self.service.poll_ready(cx)
261    }
262
263    fn call(&self, req: ServiceRequest) -> Self::Future {
264        let ip = (self.config.get_id)(&req);
265
266        if self.store.is_limited(&ip, &self.config) {
267            let res = (self.config.on_exceed)(&ip, &self.config, &req);
268            let res = req.into_response(res).map_into_right_body();
269            return Box::pin(async { Ok(res) });
270        }
271
272        let fut = self.service.call(req);
273        Box::pin(async move {
274            let res = fut.await?;
275            Ok(res.map_into_left_body())
276        })
277    }
278}