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 pub const fn count() -> usize {
112 COUNT
113 }
114
115 pub const fn per() -> u64 {
117 PER
118 }
119
120 pub const fn quota() -> Quota {
122 Quota::new(COUNT, PER)
123 }
124
125 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 Limit => TokenBucketPolicy
177}
178
179define_limit_extractor! {
180 FixedWindowLimit => FixedWindowPolicy
182}
183
184define_limit_extractor! {
185 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 pub fn resolved_quota(&self) -> Quota {
265 self.1
266 }
267
268 pub fn into_parts(self) -> (K::Extractor, Quota) {
270 (self.0, self.1)
271 }
272
273 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 DynamicLimit => TokenBucketPolicy
342}
343
344define_dynamic_limit_extractor! {
345 DynamicFixedWindowLimit => FixedWindowPolicy
347}
348
349define_dynamic_limit_extractor! {
350 DynamicSlidingWindowLimit => SlidingWindowPolicy
352}
353
354pub type LimitPerSecond<const COUNT: usize, K, B = MemoryBackend> = Limit<COUNT, 1000, K, B>;
356
357pub type LimitPerMinute<const COUNT: usize, K, B = MemoryBackend> = Limit<COUNT, 60_000, K, B>;
359
360pub type LimitPerHour<const COUNT: usize, K, B = MemoryBackend> = Limit<COUNT, 3_600_000, K, B>;
362
363pub type LimitPerDay<const COUNT: usize, K, B = MemoryBackend> = Limit<COUNT, 86_400_000, K, B>;
365
366pub type FixedWindowPerSecond<const COUNT: usize, K, B = MemoryBackend> =
368 FixedWindowLimit<COUNT, 1000, K, B>;
369
370pub type FixedWindowPerMinute<const COUNT: usize, K, B = MemoryBackend> =
372 FixedWindowLimit<COUNT, 60_000, K, B>;
373
374pub type FixedWindowPerHour<const COUNT: usize, K, B = MemoryBackend> =
376 FixedWindowLimit<COUNT, 3_600_000, K, B>;
377
378pub type FixedWindowPerDay<const COUNT: usize, K, B = MemoryBackend> =
380 FixedWindowLimit<COUNT, 86_400_000, K, B>;
381
382pub type SlidingWindowPerSecond<const COUNT: usize, K, B = MemoryBackend> =
384 SlidingWindowLimit<COUNT, 1000, K, B>;
385
386pub type SlidingWindowPerMinute<const COUNT: usize, K, B = MemoryBackend> =
388 SlidingWindowLimit<COUNT, 60_000, K, B>;
389
390pub type SlidingWindowPerHour<const COUNT: usize, K, B = MemoryBackend> =
392 SlidingWindowLimit<COUNT, 3_600_000, K, B>;
393
394pub type SlidingWindowPerDay<const COUNT: usize, K, B = MemoryBackend> =
396 SlidingWindowLimit<COUNT, 86_400_000, K, B>;
397
398#[async_trait::async_trait]
401pub trait Key: Send + Sync {
402 type Extractor;
404 fn from_extractor(extractor: &Self::Extractor) -> Self;
406}
407
408#[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 pub fn new(backend: B) -> Self {
423 Self {
424 backend,
425 _marker: PhantomData,
426 }
427 }
428
429 pub fn backend(&self) -> &B {
431 &self.backend
432 }
433
434 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#[derive(Debug)]
459pub enum LimitRejection<K, Q = Infallible> {
460 KeyExtractionFailure(K),
462
463 QuotaResolutionFailure(Q),
465
466 RateLimitExceeded(RateLimitSnapshot),
468
469 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
515pub 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}