@@ -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