Skip to main content

axum_limit/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(unsafe_code, missing_docs, clippy::unwrap_used)]
3
4mod backend;
5mod codec;
6mod key;
7mod policy;
8mod quota;
9mod quota_source;
10mod snapshot;
11mod time;
12
13use axum_core::extract::{FromRef, FromRequestParts};
14use axum_core::response::{IntoResponse, Response};
15use http::request::Parts;
16use http::StatusCode;
17use std::convert::Infallible;
18use std::error::Error;
19use std::fmt::Display;
20use std::marker::PhantomData;
21use std::ops::{Deref, DerefMut};
22use time::now_ms;
23
24#[cfg(feature = "redis")]
25pub use backend::RedisBackend;
26pub use backend::{
27    apply_policy, build_storage_key, BackendError, MemoryBackend, RateLimitBackend, StorageKey,
28};
29pub use codec::CodecError;
30pub use policy::{
31    FixedWindowPolicy, PolicyState, RateLimitPolicy, SlidingWindowPolicy, TokenBucketPolicy,
32};
33pub use quota::{Quota, QuotaFingerprint};
34pub use quota_source::{FixedQuota, QuotaSource};
35pub use snapshot::{RateLimitInfo, RateLimitSnapshot};
36
37macro_rules! define_limit_extractor {
38    (
39        $(#[$struct_meta:meta])*
40        $name:ident => $policy:ty
41    ) => {
42        $(#[$struct_meta])*
43        #[derive(Debug, Clone, Copy, Default)]
44        pub struct $name<const COUNT: usize, const PER: u64, K, B = MemoryBackend>(
45            pub K::Extractor,
46            pub std::marker::PhantomData<B>,
47        )
48        where
49            K: Key,
50            B: RateLimitBackend;
51
52        impl<const COUNT: usize, const PER: u64, K, B> AsRef<K::Extractor> for $name<COUNT, PER, K, B>
53        where
54            K: Key,
55            B: RateLimitBackend,
56        {
57            fn as_ref(&self) -> &K::Extractor {
58                &self.0
59            }
60        }
61
62        impl<const COUNT: usize, const PER: u64, K, B> AsMut<K::Extractor> for $name<COUNT, PER, K, B>
63        where
64            K: Key,
65            B: RateLimitBackend,
66        {
67            fn as_mut(&mut self) -> &mut K::Extractor {
68                &mut self.0
69            }
70        }
71
72        impl<const COUNT: usize, const PER: u64, K, B> Deref for $name<COUNT, PER, K, B>
73        where
74            K: Key,
75            B: RateLimitBackend,
76        {
77            type Target = K::Extractor;
78
79            fn deref(&self) -> &Self::Target {
80                &self.0
81            }
82        }
83
84        impl<const COUNT: usize, const PER: u64, K, B> DerefMut for $name<COUNT, PER, K, B>
85        where
86            K: Key,
87            B: RateLimitBackend,
88        {
89            fn deref_mut(&mut self) -> &mut Self::Target {
90                &mut self.0
91            }
92        }
93
94        impl<const COUNT: usize, const PER: u64, K, B> Display for $name<COUNT, PER, K, B>
95        where
96            K: Key,
97            B: RateLimitBackend,
98            K::Extractor: Display,
99        {
100            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101                self.0.fmt(f)
102            }
103        }
104
105        impl<const COUNT: usize, const PER: u64, K, B> $name<COUNT, PER, K, B>
106        where
107            K: Key,
108            B: RateLimitBackend,
109        {
110            /// Returns the count of requests allowed within the specified period.
111            pub const fn count() -> usize {
112                COUNT
113            }
114
115            /// Returns the period (in milliseconds) for which the limit applies.
116            pub const fn per() -> u64 {
117                PER
118            }
119
120            /// Returns the quota configured by this extractor.
121            pub const fn quota() -> Quota {
122                Quota::new(COUNT, PER)
123            }
124
125            /// Consumes the limit and returns the inner extractor.
126            pub fn into_inner(self) -> K::Extractor {
127                self.0
128            }
129        }
130
131        impl<const C: usize, const P: u64, K, B, S> FromRequestParts<S> for $name<C, P, K, B>
132        where
133            B: RateLimitBackend,
134            B::Error: Display,
135            S: Send + Sync,
136            K: Key + StorageKey,
137            K::Extractor: FromRequestParts<S> + Send,
138            LimitState<K, $policy, B>: FromRef<S>,
139        {
140            type Rejection =
141                LimitRejection<<<K as Key>::Extractor as FromRequestParts<S>>::Rejection>;
142
143            fn from_request_parts(
144                parts: &mut Parts,
145                state: &S,
146            ) -> impl std::future::Future<Output = Result<Self, Self::Rejection>> + Send {
147                async move {
148                    let key_extractor = match K::Extractor::from_request_parts(parts, state).await {
149                        Ok(ke) => ke,
150                        Err(rejection) => {
151                            return Err(LimitRejection::KeyExtractionFailure(rejection));
152                        }
153                    };
154
155                    let limit_state: LimitState<K, $policy, B> = FromRef::from_ref(state);
156                    let key = K::from_extractor(&key_extractor);
157                    let snapshot = limit_state
158                        .check(key, Quota::new(C, P))
159                        .await
160                        .map_err(|error| LimitRejection::Backend(error.to_string()))?;
161
162                    if snapshot.allowed {
163                        parts.extensions.insert(RateLimitInfo(snapshot));
164                        Ok(Self(key_extractor, std::marker::PhantomData))
165                    } else {
166                        Err(LimitRejection::RateLimitExceeded(snapshot))
167                    }
168                }
169            }
170        }
171    };
172}
173
174define_limit_extractor! {
175    /// Token-bucket rate limit extractor.
176    Limit => TokenBucketPolicy
177}
178
179define_limit_extractor! {
180    /// Fixed-window rate limit extractor.
181    FixedWindowLimit => FixedWindowPolicy
182}
183
184define_limit_extractor! {
185    /// Sliding-window rate limit extractor.
186    SlidingWindowLimit => SlidingWindowPolicy
187}
188
189macro_rules! define_dynamic_limit_extractor {
190    (
191        $(#[$struct_meta:meta])*
192        $name:ident => $policy:ty
193    ) => {
194        $(#[$struct_meta])*
195        #[derive(Debug, Clone)]
196        pub struct $name<K, Q, B = MemoryBackend>(
197            pub K::Extractor,
198            pub Quota,
199            pub std::marker::PhantomData<(Q, B)>,
200        )
201        where
202            K: Key,
203            B: RateLimitBackend;
204
205        impl<K, Q, B> AsRef<K::Extractor> for $name<K, Q, B>
206        where
207            K: Key,
208            B: RateLimitBackend,
209        {
210            fn as_ref(&self) -> &K::Extractor {
211                &self.0
212            }
213        }
214
215        impl<K, Q, B> AsMut<K::Extractor> for $name<K, Q, B>
216        where
217            K: Key,
218            B: RateLimitBackend,
219        {
220            fn as_mut(&mut self) -> &mut K::Extractor {
221                &mut self.0
222            }
223        }
224
225        impl<K, Q, B> Deref for $name<K, Q, B>
226        where
227            K: Key,
228            B: RateLimitBackend,
229        {
230            type Target = K::Extractor;
231
232            fn deref(&self) -> &Self::Target {
233                &self.0
234            }
235        }
236
237        impl<K, Q, B> DerefMut for $name<K, Q, B>
238        where
239            K: Key,
240            B: RateLimitBackend,
241        {
242            fn deref_mut(&mut self) -> &mut Self::Target {
243                &mut self.0
244            }
245        }
246
247        impl<K, Q, B> Display for $name<K, Q, B>
248        where
249            K: Key,
250            B: RateLimitBackend,
251            K::Extractor: Display,
252        {
253            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254                self.0.fmt(f)
255            }
256        }
257
258        impl<K, Q, B> $name<K, Q, B>
259        where
260            K: Key,
261            B: RateLimitBackend,
262        {
263            /// Returns the quota resolved for this request.
264            pub fn resolved_quota(&self) -> Quota {
265                self.1
266            }
267
268            /// Consumes the limit and returns the inner extractor and resolved quota.
269            pub fn into_parts(self) -> (K::Extractor, Quota) {
270                (self.0, self.1)
271            }
272
273            /// Consumes the limit and returns the inner extractor.
274            pub fn into_inner(self) -> K::Extractor {
275                self.0
276            }
277        }
278
279        impl<K, Q, B, S> FromRequestParts<S> for $name<K, Q, B>
280        where
281            B: RateLimitBackend,
282            B::Error: Display,
283            S: Send + Sync,
284            K: Key + StorageKey,
285            K::Extractor: FromRequestParts<S> + Send,
286            Q: QuotaSource<S>,
287            LimitState<K, $policy, B>: FromRef<S>,
288        {
289            type Rejection = LimitRejection<
290                <<K as Key>::Extractor as FromRequestParts<S>>::Rejection,
291                Q::Rejection,
292            >;
293
294            fn from_request_parts(
295                parts: &mut Parts,
296                state: &S,
297            ) -> impl std::future::Future<Output = Result<Self, Self::Rejection>> + Send
298            where
299                S: Sync,
300            {
301                async move {
302                    let key_extractor = match K::Extractor::from_request_parts(parts, state).await {
303                        Ok(ke) => ke,
304                        Err(rejection) => {
305                            return Err(LimitRejection::KeyExtractionFailure(rejection));
306                        }
307                    };
308
309                    let quota = match Q::resolve(parts, state).await {
310                        Ok(quota) => quota,
311                        Err(rejection) => {
312                            return Err(LimitRejection::QuotaResolutionFailure(rejection));
313                        }
314                    };
315
316                    let limit_state: LimitState<K, $policy, B> = FromRef::from_ref(state);
317                    let key = K::from_extractor(&key_extractor);
318                    let snapshot = limit_state
319                        .check(key, quota)
320                        .await
321                        .map_err(|error| LimitRejection::Backend(error.to_string()))?;
322
323                    if snapshot.allowed {
324                        parts.extensions.insert(RateLimitInfo(snapshot));
325                        Ok(Self(
326                            key_extractor,
327                            quota,
328                            std::marker::PhantomData,
329                        ))
330                    } else {
331                        Err(LimitRejection::RateLimitExceeded(snapshot))
332                    }
333                }
334            }
335        }
336    };
337}
338
339define_dynamic_limit_extractor! {
340    /// Token-bucket rate limit extractor with a runtime [`QuotaSource`].
341    DynamicLimit => TokenBucketPolicy
342}
343
344define_dynamic_limit_extractor! {
345    /// Fixed-window rate limit extractor with a runtime [`QuotaSource`].
346    DynamicFixedWindowLimit => FixedWindowPolicy
347}
348
349define_dynamic_limit_extractor! {
350    /// Sliding-window rate limit extractor with a runtime [`QuotaSource`].
351    DynamicSlidingWindowLimit => SlidingWindowPolicy
352}
353
354/// Token-bucket rate limit configured to apply per second.
355pub type LimitPerSecond<const COUNT: usize, K, B = MemoryBackend> = Limit<COUNT, 1000, K, B>;
356
357/// Token-bucket rate limit configured to apply per minute.
358pub type LimitPerMinute<const COUNT: usize, K, B = MemoryBackend> = Limit<COUNT, 60_000, K, B>;
359
360/// Token-bucket rate limit configured to apply per hour.
361pub type LimitPerHour<const COUNT: usize, K, B = MemoryBackend> = Limit<COUNT, 3_600_000, K, B>;
362
363/// Token-bucket rate limit configured to apply per day.
364pub type LimitPerDay<const COUNT: usize, K, B = MemoryBackend> = Limit<COUNT, 86_400_000, K, B>;
365
366/// Fixed-window rate limit configured to apply per second.
367pub type FixedWindowPerSecond<const COUNT: usize, K, B = MemoryBackend> =
368    FixedWindowLimit<COUNT, 1000, K, B>;
369
370/// Fixed-window rate limit configured to apply per minute.
371pub type FixedWindowPerMinute<const COUNT: usize, K, B = MemoryBackend> =
372    FixedWindowLimit<COUNT, 60_000, K, B>;
373
374/// Fixed-window rate limit configured to apply per hour.
375pub type FixedWindowPerHour<const COUNT: usize, K, B = MemoryBackend> =
376    FixedWindowLimit<COUNT, 3_600_000, K, B>;
377
378/// Fixed-window rate limit configured to apply per day.
379pub type FixedWindowPerDay<const COUNT: usize, K, B = MemoryBackend> =
380    FixedWindowLimit<COUNT, 86_400_000, K, B>;
381
382/// Sliding-window rate limit configured to apply per second.
383pub type SlidingWindowPerSecond<const COUNT: usize, K, B = MemoryBackend> =
384    SlidingWindowLimit<COUNT, 1000, K, B>;
385
386/// Sliding-window rate limit configured to apply per minute.
387pub type SlidingWindowPerMinute<const COUNT: usize, K, B = MemoryBackend> =
388    SlidingWindowLimit<COUNT, 60_000, K, B>;
389
390/// Sliding-window rate limit configured to apply per hour.
391pub type SlidingWindowPerHour<const COUNT: usize, K, B = MemoryBackend> =
392    SlidingWindowLimit<COUNT, 3_600_000, K, B>;
393
394/// Sliding-window rate limit configured to apply per day.
395pub type SlidingWindowPerDay<const COUNT: usize, K, B = MemoryBackend> =
396    SlidingWindowLimit<COUNT, 86_400_000, K, B>;
397
398/// Trait defining the requirements for a key extractor, which is used to uniquely identify limit subjects
399/// and extract rate limit parameters dynamically in request processing.
400#[async_trait::async_trait]
401pub trait Key: Send + Sync {
402    /// Extractor used to build a rate limit key from request parts.
403    type Extractor;
404    /// Creates an instance of `Self` from the provided extractor reference.
405    fn from_extractor(extractor: &Self::Extractor) -> Self;
406}
407
408/// Manages per-key state for a specific [`RateLimitPolicy`] and [`RateLimitBackend`].
409#[derive(Clone)]
410pub struct LimitState<K, P = TokenBucketPolicy, B = MemoryBackend> {
411    backend: B,
412    _marker: PhantomData<(K, P)>,
413}
414
415impl<K, P, B> LimitState<K, P, B>
416where
417    K: Key,
418    P: RateLimitPolicy,
419    B: RateLimitBackend,
420{
421    /// Creates a limit state backed by the provided storage backend.
422    pub fn new(backend: B) -> Self {
423        Self {
424            backend,
425            _marker: PhantomData,
426        }
427    }
428
429    /// Returns the configured storage backend.
430    pub fn backend(&self) -> &B {
431        &self.backend
432    }
433
434    /// Checks and updates the rate limit for the given key.
435    pub async fn check(&self, key: K, quota: Quota) -> Result<RateLimitSnapshot, B::Error>
436    where
437        K: StorageKey,
438    {
439        let storage_key =
440            build_storage_key::<P>(self.backend.namespace(), &key.storage_key(), quota);
441        self.backend
442            .transact::<P>(&storage_key, quota, now_ms())
443            .await
444    }
445}
446
447impl<K, P> Default for LimitState<K, P, MemoryBackend>
448where
449    K: Key,
450    P: RateLimitPolicy,
451{
452    fn default() -> Self {
453        Self::new(MemoryBackend::default())
454    }
455}
456
457/// Enumerates possible failure modes for rate limiting when extracting from request parts.
458#[derive(Debug)]
459pub enum LimitRejection<K, Q = Infallible> {
460    /// Indicates a failure during key extraction, storing the underlying rejection reason.
461    KeyExtractionFailure(K),
462
463    /// Indicates that quota resolution failed.
464    QuotaResolutionFailure(Q),
465
466    /// Indicates that the rate limit has been exceeded.
467    RateLimitExceeded(RateLimitSnapshot),
468
469    /// Indicates that the storage backend failed while checking the limit.
470    Backend(String),
471}
472
473impl<K: Display, Q: Display> Display for LimitRejection<K, Q> {
474    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
475        match self {
476            LimitRejection::KeyExtractionFailure(r) => write!(f, "{r}"),
477            LimitRejection::QuotaResolutionFailure(r) => write!(f, "{r}"),
478            LimitRejection::RateLimitExceeded(_) => write!(f, "Rate limit exceeded."),
479            LimitRejection::Backend(message) => write!(f, "Rate limit storage failure: {message}"),
480        }
481    }
482}
483
484impl<K: Error + 'static, Q: Error + 'static> Error for LimitRejection<K, Q> {
485    fn source(&self) -> Option<&(dyn Error + 'static)> {
486        match self {
487            LimitRejection::KeyExtractionFailure(error) => Some(error),
488            LimitRejection::QuotaResolutionFailure(error) => Some(error),
489            LimitRejection::RateLimitExceeded(_) | LimitRejection::Backend(_) => None,
490        }
491    }
492}
493
494impl<K: IntoResponse, Q: IntoResponse> IntoResponse for LimitRejection<K, Q> {
495    fn into_response(self) -> Response {
496        match self {
497            LimitRejection::KeyExtractionFailure(rejection) => rejection.into_response(),
498            LimitRejection::QuotaResolutionFailure(rejection) => rejection.into_response(),
499            LimitRejection::RateLimitExceeded(snapshot) => {
500                let now_ms = now_ms();
501                let mut response =
502                    (StatusCode::TOO_MANY_REQUESTS, "Rate limit exceeded.").into_response();
503                response.headers_mut().extend(snapshot.to_headers(now_ms));
504                response
505            }
506            LimitRejection::Backend(message) => (
507                StatusCode::SERVICE_UNAVAILABLE,
508                format!("Rate limit storage failure: {message}"),
509            )
510                .into_response(),
511        }
512    }
513}
514
515/// Returns rate limit headers from request extensions when a limit check succeeded.
516pub fn rate_limit_headers_from_parts(parts: &Parts) -> Option<http::HeaderMap> {
517    parts
518        .extensions
519        .get::<RateLimitInfo>()
520        .map(|info| info.0.to_headers(now_ms()))
521}
522
523#[cfg(test)]
524mod tests {
525    use super::*;
526    use axum::routing::get;
527    use axum::Router;
528    use axum_test::TestServer;
529    use http::Uri;
530    use std::future::IntoFuture;
531    use std::time::Duration;
532
533    #[tokio::test]
534    async fn token_bucket_limit() {
535        const TEST_ROUTE0: &str = "/limit0";
536        const TEST_ROUTE1: &str = "/limit1";
537        async fn handler0(Limit(_uri, _): LimitPerSecond<1, Uri>) -> impl IntoResponse {}
538
539        async fn handler1(Limit(_uri, _): LimitPerSecond<3, Uri>) -> impl IntoResponse {}
540
541        let my_app = Router::new()
542            .route(TEST_ROUTE0, get(handler0))
543            .route(TEST_ROUTE1, get(handler1))
544            .with_state(LimitState::default());
545
546        let server = TestServer::new(my_app);
547
548        let response = server.get(TEST_ROUTE0).await;
549        assert_eq!(response.status_code(), StatusCode::OK);
550        let response = server.get(TEST_ROUTE0).await;
551        assert_eq!(response.status_code(), StatusCode::TOO_MANY_REQUESTS);
552        tokio::time::sleep(Duration::from_secs(1)).await;
553        let response = server.get(TEST_ROUTE0).await;
554        assert_eq!(response.status_code(), StatusCode::OK);
555
556        let gets = vec![
557            server.get(TEST_ROUTE1).into_future(),
558            server.get(TEST_ROUTE1).into_future(),
559            server.get(TEST_ROUTE1).into_future(),
560        ];
561
562        let resp = futures::future::join_all(gets).await;
563        assert!(!resp.iter().any(|r| !r.status_code().is_success()));
564        assert_eq!(
565            server.get(TEST_ROUTE1).await.status_code(),
566            StatusCode::TOO_MANY_REQUESTS
567        );
568        tokio::time::sleep(Duration::from_secs(1)).await;
569        let response = server.get(TEST_ROUTE1).await;
570        assert_eq!(response.status_code(), StatusCode::OK);
571    }
572
573    #[tokio::test]
574    async fn token_bucket_limit_per_second_allows_count_per_second() {
575        const ROUTE: &str = "/per_sec";
576        async fn handler(_: LimitPerSecond<5, Uri>) -> impl IntoResponse {}
577
578        let app = Router::new()
579            .route(ROUTE, get(handler))
580            .with_state(LimitState::default());
581
582        let server = TestServer::new(app);
583
584        for _ in 0..5 {
585            assert_eq!(server.get(ROUTE).await.status_code(), StatusCode::OK);
586        }
587        assert_eq!(
588            server.get(ROUTE).await.status_code(),
589            StatusCode::TOO_MANY_REQUESTS
590        );
591
592        tokio::time::sleep(Duration::from_secs(1)).await;
593        for _ in 0..5 {
594            assert_eq!(server.get(ROUTE).await.status_code(), StatusCode::OK);
595        }
596    }
597
598    #[tokio::test]
599    async fn fixed_window_limit_per_second() {
600        const ROUTE: &str = "/fixed_per_sec";
601        async fn handler(_: FixedWindowPerSecond<3, Uri>) -> impl IntoResponse {}
602
603        let app = Router::new()
604            .route(ROUTE, get(handler))
605            .with_state(LimitState::<Uri, FixedWindowPolicy>::default());
606
607        let server = TestServer::new(app);
608
609        for _ in 0..3 {
610            assert_eq!(server.get(ROUTE).await.status_code(), StatusCode::OK);
611        }
612        assert_eq!(
613            server.get(ROUTE).await.status_code(),
614            StatusCode::TOO_MANY_REQUESTS
615        );
616
617        tokio::time::sleep(Duration::from_secs(1)).await;
618
619        for _ in 0..3 {
620            assert_eq!(server.get(ROUTE).await.status_code(), StatusCode::OK);
621        }
622    }
623
624    #[tokio::test]
625    async fn sliding_window_limit_per_second() {
626        const ROUTE: &str = "/sliding_per_sec";
627        async fn handler(_: SlidingWindowPerSecond<3, Uri>) -> impl IntoResponse {}
628
629        let app = Router::new()
630            .route(ROUTE, get(handler))
631            .with_state(LimitState::<Uri, SlidingWindowPolicy>::default());
632
633        let server = TestServer::new(app);
634
635        for _ in 0..3 {
636            assert_eq!(server.get(ROUTE).await.status_code(), StatusCode::OK);
637        }
638        assert_eq!(
639            server.get(ROUTE).await.status_code(),
640            StatusCode::TOO_MANY_REQUESTS
641        );
642
643        tokio::time::sleep(Duration::from_secs(2)).await;
644
645        for _ in 0..3 {
646            assert_eq!(server.get(ROUTE).await.status_code(), StatusCode::OK);
647        }
648    }
649
650    #[tokio::test]
651    async fn rate_limit_exceeded_includes_headers() {
652        const ROUTE: &str = "/headers";
653        async fn handler(_: LimitPerSecond<1, Uri>) -> impl IntoResponse {}
654
655        let app = Router::new()
656            .route(ROUTE, get(handler))
657            .with_state(LimitState::default());
658
659        let server = TestServer::new(app);
660
661        assert_eq!(server.get(ROUTE).await.status_code(), StatusCode::OK);
662        let response = server.get(ROUTE).await;
663        assert_eq!(response.status_code(), StatusCode::TOO_MANY_REQUESTS);
664        assert!(response.headers().get("x-ratelimit-limit").is_some());
665        assert!(response.headers().get("retry-after").is_some());
666    }
667
668    #[tokio::test]
669    async fn different_quotas_on_same_key_are_isolated() {
670        let state = LimitState::<Uri>::default();
671        let uri: Uri = "/test".parse().expect("valid uri");
672        let quota_a = Quota::new(1, 1000);
673        let quota_b = Quota::new(5, 1000);
674
675        assert!(
676            state
677                .check(uri.clone(), quota_a)
678                .await
679                .expect("check")
680                .allowed
681        );
682        assert!(
683            !state
684                .check(uri.clone(), quota_a)
685                .await
686                .expect("check")
687                .allowed
688        );
689        assert!(state.check(uri, quota_b).await.expect("check").allowed);
690    }
691
692    #[tokio::test]
693    async fn custom_memory_namespace_isolates_state() {
694        let left = LimitState::<Uri>::new(MemoryBackend::with_namespace("left"));
695        let right = LimitState::<Uri>::new(MemoryBackend::with_namespace("right"));
696        let uri: Uri = "/shared".parse().expect("valid uri");
697        let quota = Quota::per_second(1);
698
699        assert!(left.check(uri.clone(), quota).await.expect("left").allowed);
700        assert!(!left.check(uri.clone(), quota).await.expect("left").allowed);
701        assert!(right.check(uri, quota).await.expect("right").allowed);
702    }
703
704    #[derive(Clone)]
705    struct ConfigState {
706        limits: LimitState<Uri>,
707        api_quota: Quota,
708    }
709
710    impl FromRef<ConfigState> for LimitState<Uri> {
711        fn from_ref(state: &ConfigState) -> Self {
712            state.limits.clone()
713        }
714    }
715
716    impl FromRef<ConfigState> for Quota {
717        fn from_ref(state: &ConfigState) -> Quota {
718            state.api_quota
719        }
720    }
721
722    #[derive(Clone, Copy)]
723    struct ApiQuota(Quota);
724
725    impl FromRef<ConfigState> for ApiQuota {
726        fn from_ref(state: &ConfigState) -> Self {
727            ApiQuota(state.api_quota)
728        }
729    }
730
731    impl From<ApiQuota> for Quota {
732        fn from(value: ApiQuota) -> Self {
733            value.0
734        }
735    }
736
737    #[tokio::test]
738    async fn dynamic_limit_reads_quota_from_state() {
739        const ROUTE: &str = "/dynamic";
740        async fn handler(_: DynamicLimit<Uri, Quota>) -> impl IntoResponse {}
741
742        let state = ConfigState {
743            limits: LimitState::default(),
744            api_quota: Quota::per_second(2),
745        };
746
747        let app = Router::new().route(ROUTE, get(handler)).with_state(state);
748
749        let server = TestServer::new(app);
750
751        assert_eq!(server.get(ROUTE).await.status_code(), StatusCode::OK);
752        assert_eq!(server.get(ROUTE).await.status_code(), StatusCode::OK);
753        assert_eq!(
754            server.get(ROUTE).await.status_code(),
755            StatusCode::TOO_MANY_REQUESTS
756        );
757    }
758
759    #[tokio::test]
760    async fn dynamic_limit_state_quota_newtype() {
761        const ROUTE: &str = "/dynamic_newtype";
762        async fn handler(_: DynamicLimit<Uri, ApiQuota>) -> impl IntoResponse {}
763
764        let state = ConfigState {
765            limits: LimitState::default(),
766            api_quota: Quota::per_second(1),
767        };
768
769        let app = Router::new().route(ROUTE, get(handler)).with_state(state);
770
771        let server = TestServer::new(app);
772
773        assert_eq!(server.get(ROUTE).await.status_code(), StatusCode::OK);
774        assert_eq!(
775            server.get(ROUTE).await.status_code(),
776            StatusCode::TOO_MANY_REQUESTS
777        );
778    }
779
780    #[tokio::test]
781    async fn different_keys_are_isolated() {
782        let state = LimitState::<Uri>::default();
783        let route_a: Uri = "/a".parse().expect("valid uri");
784        let route_b: Uri = "/b".parse().expect("valid uri");
785        let quota = Quota::per_second(1);
786
787        assert!(
788            state
789                .check(route_a.clone(), quota)
790                .await
791                .expect("a")
792                .allowed
793        );
794        assert!(!state.check(route_a, quota).await.expect("a").allowed);
795        assert!(state.check(route_b, quota).await.expect("b").allowed);
796    }
797
798    #[test]
799    fn rate_limit_headers_from_parts_reads_extension() {
800        let mut parts = http::Request::new(()).into_parts().0;
801        parts.extensions.insert(RateLimitInfo(RateLimitSnapshot {
802            allowed: true,
803            limit: 5,
804            remaining: 4,
805            reset_at_ms: 1_000,
806        }));
807
808        let headers = rate_limit_headers_from_parts(&parts).expect("headers");
809        assert_eq!(
810            headers
811                .get("x-ratelimit-limit")
812                .and_then(|value| value.to_str().ok()),
813            Some("5")
814        );
815        assert_eq!(
816            headers
817                .get("x-ratelimit-remaining")
818                .and_then(|value| value.to_str().ok()),
819            Some("4")
820        );
821    }
822
823    #[tokio::test]
824    async fn dynamic_fixed_window_limit_reads_quota_from_state() {
825        #[derive(Clone)]
826        struct FixedConfigState {
827            limits: LimitState<Uri, FixedWindowPolicy>,
828            api_quota: Quota,
829        }
830
831        impl FromRef<FixedConfigState> for LimitState<Uri, FixedWindowPolicy> {
832            fn from_ref(state: &FixedConfigState) -> Self {
833                state.limits.clone()
834            }
835        }
836
837        impl FromRef<FixedConfigState> for Quota {
838            fn from_ref(state: &FixedConfigState) -> Quota {
839                state.api_quota
840            }
841        }
842
843        const ROUTE: &str = "/dynamic_fixed";
844        async fn handler(_: DynamicFixedWindowLimit<Uri, Quota>) -> impl IntoResponse {}
845
846        let state = FixedConfigState {
847            limits: LimitState::<Uri, FixedWindowPolicy>::default(),
848            api_quota: Quota::per_second(2),
849        };
850
851        let app = Router::new().route(ROUTE, get(handler)).with_state(state);
852        let server = TestServer::new(app);
853
854        assert_eq!(server.get(ROUTE).await.status_code(), StatusCode::OK);
855        assert_eq!(server.get(ROUTE).await.status_code(), StatusCode::OK);
856        assert_eq!(
857            server.get(ROUTE).await.status_code(),
858            StatusCode::TOO_MANY_REQUESTS
859        );
860    }
861
862    #[tokio::test]
863    async fn dynamic_sliding_window_limit_reads_quota_from_state() {
864        #[derive(Clone)]
865        struct SlidingConfigState {
866            limits: LimitState<Uri, SlidingWindowPolicy>,
867            api_quota: Quota,
868        }
869
870        impl FromRef<SlidingConfigState> for LimitState<Uri, SlidingWindowPolicy> {
871            fn from_ref(state: &SlidingConfigState) -> Self {
872                state.limits.clone()
873            }
874        }
875
876        impl FromRef<SlidingConfigState> for Quota {
877            fn from_ref(state: &SlidingConfigState) -> Quota {
878                state.api_quota
879            }
880        }
881
882        const ROUTE: &str = "/dynamic_sliding";
883        async fn handler(_: DynamicSlidingWindowLimit<Uri, Quota>) -> impl IntoResponse {}
884
885        let state = SlidingConfigState {
886            limits: LimitState::<Uri, SlidingWindowPolicy>::default(),
887            api_quota: Quota::per_second(1),
888        };
889
890        let app = Router::new().route(ROUTE, get(handler)).with_state(state);
891        let server = TestServer::new(app);
892
893        assert_eq!(server.get(ROUTE).await.status_code(), StatusCode::OK);
894        assert_eq!(
895            server.get(ROUTE).await.status_code(),
896            StatusCode::TOO_MANY_REQUESTS
897        );
898    }
899
900    #[derive(Clone)]
901    struct FailingBackend;
902
903    #[async_trait::async_trait]
904    impl RateLimitBackend for FailingBackend {
905        type Error = BackendError;
906
907        fn namespace(&self) -> &str {
908            "failing"
909        }
910
911        async fn transact<P>(
912            &self,
913            _storage_key: &str,
914            _quota: Quota,
915            _now_ms: u64,
916        ) -> Result<RateLimitSnapshot, Self::Error>
917        where
918            P: RateLimitPolicy,
919        {
920            Err(BackendError::Contention)
921        }
922    }
923
924    #[tokio::test]
925    async fn backend_failure_returns_service_unavailable() {
926        const ROUTE: &str = "/backend_fail";
927        async fn handler(_: Limit<1, 1000, Uri, FailingBackend>) -> impl IntoResponse {}
928
929        let app = Router::new()
930            .route(ROUTE, get(handler))
931            .with_state(LimitState::<Uri, TokenBucketPolicy, FailingBackend>::new(
932                FailingBackend,
933            ));
934
935        let server = TestServer::new(app);
936        assert_eq!(
937            server.get(ROUTE).await.status_code(),
938            StatusCode::SERVICE_UNAVAILABLE
939        );
940    }
941}