Skip to content

Commit 06797eb

Browse files
committed
feat(ratelimit): add RateLimitResponse
1 parent b85aa89 commit 06797eb

1 file changed

Lines changed: 49 additions & 0 deletions

File tree

crates/ratelimit/src/lib.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,36 @@ pub enum RateLimitOutcome {
195195
},
196196
}
197197

198+
/// Flattened rate limit response for HTTP middleware.
199+
///
200+
/// Unlike [`RateLimitOutcome`], this struct always carries all fields regardless
201+
/// of whether the request was allowed, making it straightforward to populate
202+
/// `X-RateLimit-*` and `Retry-After` response headers.
203+
///
204+
/// Returned by [`AppRateLimiter::check_response`].
205+
#[derive(Debug, Clone)]
206+
pub struct RateLimitResponse {
207+
/// Whether the request is allowed.
208+
pub allowed: bool,
209+
/// Remaining requests in the current window (0 when denied).
210+
pub remaining: u64,
211+
/// Seconds until the window resets.
212+
pub reset_after_secs: u64,
213+
}
214+
215+
impl From<RateLimitOutcome> for RateLimitResponse {
216+
fn from(outcome: RateLimitOutcome) -> Self {
217+
match outcome {
218+
RateLimitOutcome::Allowed { remaining, reset_after_secs } => {
219+
Self { allowed: true, remaining, reset_after_secs }
220+
},
221+
RateLimitOutcome::Limited { retry_after_secs } => {
222+
Self { allowed: false, remaining: 0, reset_after_secs: retry_after_secs }
223+
},
224+
}
225+
}
226+
}
227+
198228
/// Distributed fixed-window rate limiter backed by a [`StorageBackend`].
199229
///
200230
/// Uses storage-backed counters with atomic CAS operations and TTL for
@@ -321,6 +351,25 @@ impl<S: StorageBackend> AppRateLimiter<S> {
321351
// Unreachable: the loop either returns or continues
322352
Err(StorageError::cas_retries_exhausted(MAX_CAS_RETRIES))
323353
}
354+
355+
/// Checks a rate limit and returns a flat [`RateLimitResponse`].
356+
///
357+
/// This is a convenience wrapper around [`check`](Self::check) that returns
358+
/// a struct with all fields always present, suitable for populating HTTP
359+
/// rate limit headers (`X-RateLimit-Remaining`, `X-RateLimit-Reset`,
360+
/// `Retry-After`).
361+
///
362+
/// # Errors
363+
///
364+
/// Same as [`check`](Self::check).
365+
pub async fn check_response(
366+
&self,
367+
category: &str,
368+
identifier: &str,
369+
policy: &RateLimitPolicy,
370+
) -> StorageResult<RateLimitResponse> {
371+
self.check(category, identifier, policy).await.map(RateLimitResponse::from)
372+
}
324373
}
325374

326375
#[cfg(test)]

0 commit comments

Comments
 (0)