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}