Skip to main content

litellm_rs/server/middleware/
rate_limit.rs

1//! Rate limiting middleware
2
3use super::helpers::middleware_gateway_error_response;
4use super::rate_limit_key_policy::effective_requests_per_minute;
5use crate::core::rate_limiter::{RateLimitReservation, get_global_rate_limiter};
6use crate::core::types::context::{RequestContext, SharedRequestContext};
7use crate::server::state::AppState;
8use crate::utils::error::gateway_error::GatewayError;
9use actix_web::body::EitherBody;
10use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform, forward_ready};
11use actix_web::http::{StatusCode, header::HeaderMap};
12use actix_web::web;
13use actix_web::{HttpMessage, HttpRequest, HttpResponse, ResponseError};
14use dashmap::DashMap;
15use futures::future::{Ready, ready};
16use std::fmt;
17use std::future::Future;
18use std::net::{IpAddr, SocketAddr};
19use std::pin::Pin;
20use std::rc::Rc;
21use std::sync::Arc;
22use std::sync::OnceLock;
23use std::time::{Duration, Instant};
24use tracing::{debug, info, warn};
25
26/// Maximum number of distinct client trackers retained by the fallback store.
27///
28/// The fallback path runs only when the global rate limiter is not initialized.
29/// Without a cap, every distinct client IP creates a new entry that lives for
30/// the entire process lifetime, which is a memory-exhaustion vector when an
31/// attacker rotates source addresses. The value matches `AuthRateLimiter`'s
32/// `DEFAULT_MAX_ENTRIES`.
33const MAX_FALLBACK_ENTRIES: usize = 10_000;
34
35static GATEWAY_FALLBACK_STORE: OnceLock<Arc<DashMap<String, KeyTracker>>> = OnceLock::new();
36
37const GLOBAL_LIMITER_SOURCE: &str = "global";
38const FALLBACK_LIMITER_SOURCE: &str = "fallback";
39
40/// Fallback per-key tracker for sliding window when global rate limiter is unavailable
41struct KeyTracker {
42    timestamps: Vec<Instant>,
43}
44
45impl KeyTracker {
46    fn new() -> Self {
47        Self {
48            timestamps: Vec::new(),
49        }
50    }
51
52    fn release(&mut self, recorded_at: Instant) -> bool {
53        let Some(position) = self.timestamps.iter().position(|&ts| ts == recorded_at) else {
54            return false;
55        };
56
57        self.timestamps.remove(position);
58        true
59    }
60
61    /// Check-and-record atomically: returns (allowed, retry_after_secs, recorded_at)
62    fn check_and_record(&mut self, limit: u32, window: Duration) -> (bool, u64, Option<Instant>) {
63        let now = Instant::now();
64        self.timestamps
65            .retain(|&ts| now.duration_since(ts) < window);
66
67        let count = self.timestamps.len() as u32;
68        if count >= limit {
69            let retry_after = self
70                .timestamps
71                .first()
72                .map(|&ts| {
73                    let age = now.duration_since(ts);
74                    window.saturating_sub(age).as_secs().max(1)
75                })
76                .unwrap_or(window.as_secs());
77            return (false, retry_after, None);
78        }
79
80        self.timestamps.push(now);
81        (true, 0, Some(now))
82    }
83}
84
85struct RateLimitPass {
86    source: &'static str,
87    limit: u32,
88    remaining: u32,
89    reservation: RecordedRateLimitReservation,
90}
91
92struct RateLimitRejection {
93    source: &'static str,
94    retry_after: u64,
95    limit: u32,
96}
97
98enum RateLimitReservationSource {
99    Global(RateLimitReservation),
100    Fallback {
101        store: Arc<DashMap<String, KeyTracker>>,
102        recorded_at: Instant,
103    },
104    Noop,
105}
106
107enum RecordedRateLimitReservation {
108    Global(RateLimitReservation),
109    Fallback(Instant),
110}
111
112pub(super) struct AuthRateLimitReservation {
113    key: String,
114    source: RateLimitReservationSource,
115}
116
117impl AuthRateLimitReservation {
118    pub(super) fn noop() -> Self {
119        Self {
120            key: String::new(),
121            source: RateLimitReservationSource::Noop,
122        }
123    }
124
125    pub(super) async fn release(self) {
126        match self.source {
127            RateLimitReservationSource::Global(record_source) => {
128                if let Some(global_limiter) = get_global_rate_limiter() {
129                    global_limiter
130                        .release_recorded(&self.key, record_source)
131                        .await;
132                }
133            }
134            RateLimitReservationSource::Fallback { store, recorded_at } => {
135                let _removed_entry = store.remove_if_mut(&self.key, |_, tracker| {
136                    tracker.release(recorded_at);
137                    tracker.timestamps.is_empty()
138                });
139            }
140            RateLimitReservationSource::Noop => {}
141        }
142    }
143}
144
145/// Evict trackers when the fallback store exceeds the cap.
146///
147/// Two-pass strategy: first drop trackers whose latest timestamp is already
148/// outside the rate-limit window (they would re-allow on the next request
149/// anyway). If that does not free enough room, drop the trackers whose most
150/// recent activity is oldest until the map is back under cap.
151fn enforce_fallback_capacity(store: &DashMap<String, KeyTracker>, window: Duration) {
152    let now = Instant::now();
153    store.retain(|_, tracker| {
154        tracker
155            .timestamps
156            .last()
157            .is_some_and(|ts| now.duration_since(*ts) < window)
158    });
159
160    let overflow = store.len().saturating_sub(MAX_FALLBACK_ENTRIES);
161    if overflow == 0 {
162        return;
163    }
164
165    let mut candidates: Vec<(Option<Instant>, String)> = store
166        .iter()
167        .map(|e| (e.value().timestamps.last().copied(), e.key().clone()))
168        .collect();
169    // Smallest (oldest) first; None first.
170    candidates.sort_by_key(|(ts, _)| *ts);
171    for (_, key) in candidates.into_iter().take(overflow) {
172        store.remove(&key);
173    }
174}
175
176async fn check_rate_limit_key(
177    key: &str,
178    requests_per_minute: u32,
179    fallback_store: &DashMap<String, KeyTracker>,
180) -> Result<RateLimitPass, RateLimitRejection> {
181    if let Some(global_limiter) = get_global_rate_limiter() {
182        let (result, reservation) = global_limiter
183            .check_and_record_with_source_and_limit(key, requests_per_minute)
184            .await;
185
186        if !result.allowed {
187            return Err(RateLimitRejection {
188                source: GLOBAL_LIMITER_SOURCE,
189                retry_after: result.retry_after_secs.unwrap_or(60),
190                limit: requests_per_minute,
191            });
192        }
193
194        return Ok(RateLimitPass {
195            source: GLOBAL_LIMITER_SOURCE,
196            limit: requests_per_minute,
197            remaining: result.remaining,
198            reservation: RecordedRateLimitReservation::Global(reservation),
199        });
200    }
201
202    let window = Duration::from_secs(60);
203    let (allowed, retry_after, recorded_at) = {
204        let mut tracker = fallback_store
205            .entry(key.to_string())
206            .or_insert_with(KeyTracker::new);
207        tracker.check_and_record(requests_per_minute, window)
208    };
209    if fallback_store.len() > MAX_FALLBACK_ENTRIES {
210        enforce_fallback_capacity(fallback_store, window);
211    }
212
213    if !allowed {
214        return Err(RateLimitRejection {
215            source: FALLBACK_LIMITER_SOURCE,
216            retry_after,
217            limit: requests_per_minute,
218        });
219    }
220
221    let Some(recorded_at) = recorded_at else {
222        return Err(RateLimitRejection {
223            source: FALLBACK_LIMITER_SOURCE,
224            retry_after: window.as_secs().max(1),
225            limit: requests_per_minute,
226        });
227    };
228
229    let remaining = fallback_store
230        .get(key)
231        .map(|tracker| requests_per_minute.saturating_sub(tracker.timestamps.len() as u32))
232        .unwrap_or(requests_per_minute);
233
234    Ok(RateLimitPass {
235        source: FALLBACK_LIMITER_SOURCE,
236        limit: requests_per_minute,
237        remaining,
238        reservation: RecordedRateLimitReservation::Fallback(recorded_at),
239    })
240}
241
242fn gateway_fallback_store() -> Arc<DashMap<String, KeyTracker>> {
243    GATEWAY_FALLBACK_STORE
244        .get_or_init(|| Arc::new(DashMap::new()))
245        .clone()
246}
247
248/// Apply the same authenticated identity and limiter to billable socket events.
249#[cfg(feature = "websockets")]
250pub(crate) async fn enforce_socket_request_rate(
251    context: &RequestContext,
252    requests_per_minute: u32,
253) -> Result<(), u64> {
254    let key = client_key_from_context(context).ok_or(60u64)?;
255    check_rate_limit_key(&key, requests_per_minute, &gateway_fallback_store())
256        .await
257        .map(|_| ())
258        .map_err(|rejection| rejection.retry_after)
259}
260
261pub(super) async fn reserve_rate_limit_for_auth_attempt(
262    req: &ServiceRequest,
263    requests_per_minute: u32,
264    trusted_proxies: &[String],
265) -> Result<AuthRateLimitReservation, RateLimitError> {
266    let key = extract_client_key(req, trusted_proxies);
267    let fallback_store = gateway_fallback_store();
268
269    match check_rate_limit_key(&key, requests_per_minute, &fallback_store).await {
270        Ok(pass) => {
271            debug!(
272                client = %key,
273                limit = pass.limit,
274                remaining = pass.remaining,
275                "Rate limit reservation passed for auth attempt ({} limiter)",
276                pass.source
277            );
278            let source = match pass.reservation {
279                RecordedRateLimitReservation::Global(reservation) => {
280                    RateLimitReservationSource::Global(reservation)
281                }
282                RecordedRateLimitReservation::Fallback(recorded_at) => {
283                    RateLimitReservationSource::Fallback {
284                        store: fallback_store,
285                        recorded_at,
286                    }
287                }
288            };
289            Ok(AuthRateLimitReservation { key, source })
290        }
291        Err(rejection) => {
292            warn!(
293                client = %key,
294                "Rate limit exceeded before auth verification ({} limiter): retry after {}s",
295                rejection.source,
296                rejection.retry_after
297            );
298            Err(RateLimitError::new(rejection.retry_after, rejection.limit))
299        }
300    }
301}
302
303pub(super) async fn enforce_rate_limit_for_rejected_auth(
304    req: &ServiceRequest,
305    requests_per_minute: u32,
306    trusted_proxies: &[String],
307) -> Result<(), RateLimitError> {
308    let key = extract_client_key(req, trusted_proxies);
309    let fallback_store = gateway_fallback_store();
310
311    match check_rate_limit_key(&key, requests_per_minute, &fallback_store).await {
312        Ok(pass) => {
313            debug!(
314                client = %key,
315                limit = pass.limit,
316                remaining = pass.remaining,
317                "Rate limit check passed for rejected auth path ({} limiter)",
318                pass.source
319            );
320            Ok(())
321        }
322        Err(rejection) => {
323            warn!(
324                client = %key,
325                "Rate limit exceeded for rejected auth path ({} limiter): retry after {}s",
326                rejection.source,
327                rejection.retry_after
328            );
329            Err(RateLimitError::new(rejection.retry_after, rejection.limit))
330        }
331    }
332}
333
334/// Lightweight in-process rate limit error for 429 responses
335#[derive(Debug, Clone, Copy)]
336pub(super) struct RateLimitError {
337    retry_after: u64,
338    limit: u32,
339}
340
341impl RateLimitError {
342    fn new(retry_after: u64, limit: u32) -> Self {
343        Self { retry_after, limit }
344    }
345
346    pub(super) fn gateway_error(&self) -> GatewayError {
347        GatewayError::RateLimit {
348            message: "Rate limit exceeded. Please retry after the indicated seconds.".to_string(),
349            retry_after: Some(self.retry_after),
350            rpm_limit: Some(self.limit),
351            tpm_limit: None,
352        }
353    }
354}
355
356impl fmt::Display for RateLimitError {
357    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358        write!(f, "Too Many Requests")
359    }
360}
361
362impl ResponseError for RateLimitError {
363    fn status_code(&self) -> StatusCode {
364        StatusCode::TOO_MANY_REQUESTS
365    }
366
367    fn error_response(&self) -> HttpResponse {
368        HttpResponse::TooManyRequests()
369            .insert_header(("Retry-After", self.retry_after.to_string()))
370            .insert_header(("X-RateLimit-Limit", self.limit.to_string()))
371            .json(serde_json::json!({
372                "error": {
373                    "message": "Rate limit exceeded. Please retry after the indicated seconds.",
374                    "type": "rate_limit_error",
375                    "code": 429
376                }
377            }))
378    }
379}
380
381/// Rate limit middleware for Actix-web
382pub struct RateLimitMiddleware {
383    requests_per_minute: Option<u32>,
384}
385
386impl RateLimitMiddleware {
387    pub fn new(requests_per_minute: u32) -> Self {
388        Self {
389            requests_per_minute: Some(requests_per_minute),
390        }
391    }
392
393    pub fn optional(requests_per_minute: Option<u32>) -> Self {
394        Self {
395            requests_per_minute,
396        }
397    }
398}
399
400impl Default for RateLimitMiddleware {
401    fn default() -> Self {
402        Self::new(60)
403    }
404}
405
406impl<S, B> Transform<S, ServiceRequest> for RateLimitMiddleware
407where
408    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
409    S::Future: 'static,
410    B: 'static,
411{
412    type Response = ServiceResponse<EitherBody<B>>;
413    type Error = actix_web::Error;
414    type InitError = ();
415    type Transform = RateLimitMiddlewareService<S>;
416    type Future = Ready<Result<Self::Transform, Self::InitError>>;
417
418    fn new_transform(&self, service: S) -> Self::Future {
419        ready(Ok(RateLimitMiddlewareService {
420            service: Rc::new(service),
421            requests_per_minute: self.requests_per_minute,
422            fallback_store: gateway_fallback_store(),
423        }))
424    }
425}
426
427/// Service implementation for rate limit middleware
428pub struct RateLimitMiddlewareService<S> {
429    service: Rc<S>,
430    requests_per_minute: Option<u32>,
431    /// Fallback in-process store used when the global rate limiter is not initialized
432    fallback_store: Arc<DashMap<String, KeyTracker>>,
433}
434
435/// Parse an IP address, optionally accompanied by a port.
436///
437/// Handles bare IPv4/IPv6 addresses and socket addresses such as
438/// `1.2.3.4:5678` and `[::1]:5678`.
439fn parse_ip(value: &str) -> Option<IpAddr> {
440    let value = value.trim();
441    value
442        .parse::<IpAddr>()
443        .ok()
444        .or_else(|| value.parse::<SocketAddr>().ok().map(|addr| addr.ip()))
445        .map(|ip| ip.to_canonical())
446}
447
448pub(crate) fn is_trusted_proxy(ip: IpAddr, trusted_proxies: &[String]) -> bool {
449    trusted_proxies
450        .iter()
451        .filter_map(|proxy| parse_ip(proxy))
452        .any(|proxy| proxy == ip)
453}
454
455/// Extract a client identifier from the request.
456///
457/// Priority:
458/// 1. Authenticated API key ID / user ID from `RequestContext`, when present
459/// 2. `X-Forwarded-For` rightmost non-trusted address — only when peer IP is
460///    in `trusted_proxies` (leftmost entries are attacker-controlled)
461/// 3. Direct peer address from connection info
462fn extract_client_key(req: &ServiceRequest, trusted_proxies: &[String]) -> String {
463    if let Some(identity) = authenticated_client_key(req) {
464        return identity;
465    }
466
467    network_client_key(req, trusted_proxies)
468}
469
470fn authenticated_client_key(req: &ServiceRequest) -> Option<String> {
471    let extensions = req.extensions();
472    if let Some(context) = extensions.get::<SharedRequestContext>() {
473        return client_key_from_context(context.as_ref());
474    }
475
476    let context = extensions.get::<RequestContext>()?;
477    client_key_from_context(context)
478}
479
480fn client_key_from_context(context: &RequestContext) -> Option<String> {
481    if let Some(api_key_id) = context.api_key_id() {
482        return Some(format!("api_key:{}", api_key_id));
483    }
484
485    context
486        .user_id
487        .as_deref()
488        .map(str::trim)
489        .filter(|user_id| !user_id.is_empty())
490        .map(|user_id| format!("user:{}", user_id))
491}
492
493pub(super) fn network_client_key(req: &ServiceRequest, trusted_proxies: &[String]) -> String {
494    network_client_key_from_parts(req.peer_addr(), req.headers(), trusted_proxies)
495        .unwrap_or_else(|| "ip:unknown".to_string())
496}
497
498pub(crate) fn trusted_network_client_key(
499    req: &HttpRequest,
500    trusted_proxies: &[String],
501) -> Option<String> {
502    network_client_key_from_parts(req.peer_addr(), req.headers(), trusted_proxies)
503}
504
505fn network_client_key_from_parts(
506    peer_addr: Option<SocketAddr>,
507    headers: &HeaderMap,
508    trusted_proxies: &[String],
509) -> Option<String> {
510    let peer_ip = peer_addr?.ip().to_canonical();
511
512    // Only honor X-Forwarded-For when the direct peer is a trusted proxy.
513    if is_trusted_proxy(peer_ip, trusted_proxies)
514        && let Some(client_ip) = last_untrusted_xff_ip(headers, trusted_proxies)
515    {
516        return Some(format!("ip:{}", client_ip));
517    }
518
519    Some(format!("ip:{}", peer_ip))
520}
521
522/// Pick the client IP from all `X-Forwarded-For` fields by walking from the
523/// last field's last hop toward the first and skipping trusted proxies.
524///
525/// Everything left of the rightmost non-trusted entry is attacker-controlled:
526/// a client can seed `X-Forwarded-For` with arbitrary addresses before the
527/// request reaches the trusted proxy. Using the leftmost entry therefore lets
528/// clients rotate identities to dodge per-IP limits. A malformed entry in the
529/// trusted suffix rejects the header so parsing cannot continue into the
530/// attacker-controlled prefix.
531fn last_untrusted_xff_ip(headers: &HeaderMap, trusted_proxies: &[String]) -> Option<IpAddr> {
532    let mut fallback = None;
533    for field in headers.get_all("X-Forwarded-For").rev() {
534        for entry in field.as_bytes().rsplit(|byte| *byte == b',') {
535            let entry = std::str::from_utf8(entry).ok()?;
536            let ip = parse_ip(entry)?;
537            fallback.get_or_insert(ip);
538            if !is_trusted_proxy(ip, trusted_proxies) {
539                return Some(ip);
540            }
541        }
542    }
543    fallback
544}
545
546impl<S, B> Service<ServiceRequest> for RateLimitMiddlewareService<S>
547where
548    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
549    S::Future: 'static,
550    B: 'static,
551{
552    type Response = ServiceResponse<EitherBody<B>>;
553    type Error = actix_web::Error;
554    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
555
556    forward_ready!(service);
557
558    fn call(&self, req: ServiceRequest) -> Self::Future {
559        let service = Rc::clone(&self.service);
560        let app_state = req.app_data::<web::Data<AppState>>().cloned();
561        let trusted_proxies: Vec<String> = match app_state.as_ref() {
562            Some(state) => {
563                let cfg = state.config.load();
564                cfg.server().trusted_proxies.clone()
565            }
566            None => Vec::new(),
567        };
568        let start_time = Instant::now();
569        let path = req.path().to_string();
570        let method = req.method().to_string();
571
572        let client_key = extract_client_key(&req, &trusted_proxies);
573        let requests_per_minute = effective_requests_per_minute(&req, self.requests_per_minute);
574
575        let fallback_store = self.fallback_store.clone();
576        let key = client_key.clone();
577
578        Box::pin(async move {
579            if let Some(requests_per_minute) = requests_per_minute {
580                let pass =
581                    match check_rate_limit_key(&key, requests_per_minute, &fallback_store).await {
582                        Ok(pass) => pass,
583                        Err(rejection) => {
584                            warn!(
585                                client = %key,
586                                path = %path,
587                                "Rate limit exceeded ({} limiter): retry after {}s",
588                                rejection.source,
589                                rejection.retry_after
590                            );
591                            let err = RateLimitError::new(rejection.retry_after, rejection.limit);
592                            let gateway_error = err.gateway_error();
593                            return Ok(middleware_gateway_error_response(
594                                req,
595                                actix_web::Error::from(err),
596                                gateway_error,
597                            ));
598                        }
599                    };
600
601                debug!(
602                    client = %key,
603                    limit = pass.limit,
604                    remaining = pass.remaining,
605                    "Rate limit check passed ({} limiter)",
606                    pass.source
607                );
608            }
609
610            let res = service.call(req).await?.map_into_left_body();
611            let duration = start_time.elapsed();
612            info!(
613                "{} {} completed in {:?} with status {}",
614                method,
615                path,
616                duration,
617                res.status()
618            );
619            Ok(res)
620        })
621    }
622}
623
624#[cfg(test)]
625#[path = "rate_limit_tests.rs"]
626mod tests;