Skip to main content

rainier_middleware/
builtin.rs

1//! The middleware Rainier ships with — what a fresh application's HTTP kernel
2//! expects to find already written.
3
4use std::sync::Arc;
5use std::time::Duration;
6
7use rainier_http::{IntoResponse, Method, Request, Response, StatusCode};
8use serde_json::Value;
9
10use crate::pipeline::{Middleware, Next};
11use crate::rate_limit::{MemoryRateLimitStore, RateLimitStore};
12
13/// Trims leading and trailing whitespace from every string input.
14///
15/// A stray space at the end of an email field is the single most common cause
16/// of a "that address is already taken" that the user cannot see, so this is on
17/// by default — with an exception list, because trimming a password silently
18/// changes a credential the user typed on purpose.
19#[derive(Debug, Clone)]
20pub struct TrimStrings {
21    except: Vec<String>,
22}
23
24impl Default for TrimStrings {
25    fn default() -> Self {
26        Self { except: vec!["password".into(), "password_confirmation".into()] }
27    }
28}
29
30impl TrimStrings {
31    /// Trim everything except `password` and `password_confirmation`.
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    /// Replace the exception list.
37    pub fn except(mut self, keys: impl IntoIterator<Item = impl Into<String>>) -> Self {
38        self.except = keys.into_iter().map(Into::into).collect();
39        self
40    }
41
42    fn apply(&self, value: Value) -> Value {
43        match value {
44            Value::String(s) => Value::String(s.trim().to_string()),
45            Value::Array(items) => {
46                Value::Array(items.into_iter().map(|item| self.apply(item)).collect())
47            }
48            Value::Object(map) => Value::Object(
49                map.into_iter()
50                    .map(|(key, item)| {
51                        let trimmed =
52                            if self.except.contains(&key) { item } else { self.apply(item) };
53                        (key, trimmed)
54                    })
55                    .collect(),
56            ),
57            other => other,
58        }
59    }
60}
61
62#[async_trait::async_trait]
63impl Middleware for TrimStrings {
64    async fn handle(&self, mut request: Request, next: Next) -> Response {
65        request.transform_input(|input| self.apply(input));
66        next.run(request).await
67    }
68
69    fn name(&self) -> &'static str {
70        "TrimStrings"
71    }
72}
73
74/// Turns empty string inputs into `null`.
75///
76/// An unfilled HTML text input submits `""`, not nothing at all. Without this,
77/// every optional field in an application would need `Option<String>` plus an
78/// is-it-blank check, and a `Option<u32>` field could never parse. Paired with
79/// [`TrimStrings`], a whitespace-only field also becomes `null`.
80#[derive(Debug, Clone, Default)]
81pub struct ConvertEmptyStringsToNull;
82
83impl ConvertEmptyStringsToNull {
84    fn apply(value: Value) -> Value {
85        match value {
86            Value::String(s) if s.is_empty() => Value::Null,
87            Value::Array(items) => Value::Array(items.into_iter().map(Self::apply).collect()),
88            Value::Object(map) => {
89                Value::Object(map.into_iter().map(|(k, v)| (k, Self::apply(v))).collect())
90            }
91            other => other,
92        }
93    }
94}
95
96#[async_trait::async_trait]
97impl Middleware for ConvertEmptyStringsToNull {
98    async fn handle(&self, mut request: Request, next: Next) -> Response {
99        request.transform_input(Self::apply);
100        next.run(request).await
101    }
102
103    fn name(&self) -> &'static str {
104        "ConvertEmptyStringsToNull"
105    }
106}
107
108/// Adds fixed headers to every response — security headers, a server tag.
109#[derive(Debug, Clone, Default)]
110pub struct AddHeaders {
111    headers: Vec<(String, String)>,
112}
113
114impl AddHeaders {
115    /// No headers yet.
116    pub fn new() -> Self {
117        Self::default()
118    }
119
120    /// A conservative set of security headers suitable for an HTML app.
121    ///
122    /// Deliberately excludes `Strict-Transport-Security` and a
123    /// `Content-Security-Policy`: HSTS is unsafe to set before HTTPS is
124    /// definitely working everywhere, and a CSP that is not tailored to the app
125    /// either breaks it or is too loose to help.
126    pub fn security_defaults() -> Self {
127        Self::new()
128            .with("x-content-type-options", "nosniff")
129            .with("x-frame-options", "SAMEORIGIN")
130            .with("referrer-policy", "strict-origin-when-cross-origin")
131    }
132
133    /// Add a header.
134    pub fn with(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
135        self.headers.push((name.into(), value.into()));
136        self
137    }
138}
139
140#[async_trait::async_trait]
141impl Middleware for AddHeaders {
142    async fn handle(&self, request: Request, next: Next) -> Response {
143        let mut response = next.run(request).await;
144        for (name, value) in &self.headers {
145            response = response.with_header(name, value);
146        }
147        response
148    }
149
150    fn name(&self) -> &'static str {
151        "AddHeaders"
152    }
153}
154
155/// Which origins a [`HandleCors`] policy allows.
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub enum AllowedOrigins {
158    /// Every origin, echoed back. Cannot be combined with credentials.
159    Any,
160    /// Only these exact origins.
161    List(Vec<String>),
162}
163
164/// Cross-Origin Resource Sharing.
165///
166/// Answers preflight `OPTIONS` requests itself (short-circuiting the pipeline,
167/// which is why a preflight never reaches a route) and adds the response
168/// headers to everything else.
169#[derive(Debug, Clone)]
170pub struct HandleCors {
171    origins: AllowedOrigins,
172    methods: Vec<String>,
173    headers: Vec<String>,
174    exposed: Vec<String>,
175    credentials: bool,
176    max_age: u32,
177}
178
179impl Default for HandleCors {
180    fn default() -> Self {
181        Self {
182            origins: AllowedOrigins::Any,
183            methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
184                .iter()
185                .map(|m| m.to_string())
186                .collect(),
187            headers: ["content-type", "authorization", "x-requested-with"]
188                .iter()
189                .map(|h| h.to_string())
190                .collect(),
191            exposed: Vec::new(),
192            credentials: false,
193            max_age: 86_400,
194        }
195    }
196}
197
198impl HandleCors {
199    /// A permissive policy: any origin, no credentials.
200    pub fn any_origin() -> Self {
201        Self::default()
202    }
203
204    /// Restrict to an explicit origin list.
205    pub fn allow_origins(mut self, origins: impl IntoIterator<Item = impl Into<String>>) -> Self {
206        self.origins = AllowedOrigins::List(origins.into_iter().map(Into::into).collect());
207        self
208    }
209
210    /// Set the allowed methods.
211    pub fn allow_methods(mut self, methods: impl IntoIterator<Item = impl Into<String>>) -> Self {
212        self.methods = methods.into_iter().map(Into::into).collect();
213        self
214    }
215
216    /// Set the allowed request headers.
217    pub fn allow_headers(mut self, headers: impl IntoIterator<Item = impl Into<String>>) -> Self {
218        self.headers = headers.into_iter().map(Into::into).collect();
219        self
220    }
221
222    /// Expose extra response headers to the browser.
223    pub fn expose_headers(mut self, headers: impl IntoIterator<Item = impl Into<String>>) -> Self {
224        self.exposed = headers.into_iter().map(Into::into).collect();
225        self
226    }
227
228    /// Allow credentialed requests.
229    ///
230    /// Browsers reject `Access-Control-Allow-Credentials: true` alongside
231    /// `Access-Control-Allow-Origin: *`, so enabling this with
232    /// [`AllowedOrigins::Any`] would produce a policy that silently never
233    /// works. [`allowed_origin_for`](Self::allowed_origin_for) therefore echoes
234    /// the request's origin instead of `*` when credentials are on.
235    pub fn allow_credentials(mut self, allow: bool) -> Self {
236        self.credentials = allow;
237        self
238    }
239
240    /// How long a browser may cache the preflight result.
241    pub fn max_age(mut self, seconds: u32) -> Self {
242        self.max_age = seconds;
243        self
244    }
245
246    /// The `Access-Control-Allow-Origin` value for a request from `origin`, or
247    /// `None` if the origin is not allowed.
248    pub fn allowed_origin_for(&self, origin: Option<&str>) -> Option<String> {
249        match &self.origins {
250            AllowedOrigins::Any if self.credentials => origin.map(str::to_string),
251            AllowedOrigins::Any => Some("*".to_string()),
252            AllowedOrigins::List(allowed) => {
253                let origin = origin?;
254                allowed.iter().any(|a| a == origin).then(|| origin.to_string())
255            }
256        }
257    }
258
259    fn decorate(&self, response: Response, allowed_origin: Option<String>) -> Response {
260        let Some(origin) = allowed_origin else {
261            return response;
262        };
263
264        let mut response = response.with_header("access-control-allow-origin", &origin);
265        if origin != "*" {
266            // Caches must not serve one origin's response to another.
267            response = response.with_added_header("vary", "Origin");
268        }
269        if self.credentials {
270            response = response.with_header("access-control-allow-credentials", "true");
271        }
272        if !self.exposed.is_empty() {
273            response =
274                response.with_header("access-control-expose-headers", &self.exposed.join(", "));
275        }
276        response
277    }
278}
279
280#[async_trait::async_trait]
281impl Middleware for HandleCors {
282    async fn handle(&self, request: Request, next: Next) -> Response {
283        let origin = request.header("origin").map(str::to_string);
284        let allowed = self.allowed_origin_for(origin.as_deref());
285
286        // A preflight is answered here and never reaches a route — there is no
287        // route for `OPTIONS /whatever` and the browser only wants headers.
288        if request.method() == Method::OPTIONS
289            && request.header("access-control-request-method").is_some()
290        {
291            let response = Response::new(StatusCode::NO_CONTENT)
292                .with_header("access-control-allow-methods", &self.methods.join(", "))
293                .with_header("access-control-allow-headers", &self.headers.join(", "))
294                .with_header("access-control-max-age", &self.max_age.to_string());
295            return self.decorate(response, allowed);
296        }
297
298        let response = next.run(request).await;
299        self.decorate(response, allowed)
300    }
301
302    fn name(&self) -> &'static str {
303        "HandleCors"
304    }
305}
306
307/// A fixed-window rate limiter.
308///
309/// ```ignore
310/// // The default: this process's memory, keyed by token or address.
311/// router.get("/api/posts", index).middleware(ThrottleRequests::per_minute(60));
312///
313/// // A credential limiter: keyed by what was submitted, counted in the shared
314/// // cache, under its own name.
315/// router.post("/login", login).middleware(
316///     ThrottleRequests::per_minute(5)
317///         .named("login")
318///         .keyed_by(|request| request.input("email"))
319///         .stored_in(Arc::clone(&limits)),
320/// );
321/// ```
322///
323/// # What to key on
324///
325/// The default is the bearer token if there is one and the address otherwise,
326/// which is right for an API: one caller behind a shared NAT cannot exhaust
327/// everyone else's allowance.
328///
329/// It is the **wrong** key for a login form. There is no token yet, so every
330/// attempt counts against an address — and an attacker spraying one password
331/// across ten thousand accounts from a botnet never trips it, while a whole
332/// office behind one NAT locks itself out. Keying on the submitted address
333/// instead limits attempts *per account*, which is the thing being protected.
334///
335/// Both are worth having, on the same route, under two names. Which is why
336/// [`named`](Self::named) exists.
337#[derive(Clone)]
338pub struct ThrottleRequests {
339    max_attempts: u32,
340    window: Duration,
341    /// Namespaces the keys, so two limiters on one route do not share a
342    /// counter.
343    name: Option<String>,
344    /// How a request becomes a key. `None` is the default token-or-address
345    /// rule.
346    key_by: Option<KeyFn>,
347    store: Arc<dyn RateLimitStore>,
348}
349
350/// Turns a request into the key it should be counted under.
351///
352/// Returning `None` means **do not count this request** — it has nothing to
353/// key on, which for a login limiter is a request with no email in it and is
354/// somebody else's `422` rather than this middleware's `429`.
355type KeyFn = Arc<dyn Fn(&Request) -> Option<String> + Send + Sync>;
356
357impl ThrottleRequests {
358    /// `max_attempts` per `window`, counted in this process.
359    pub fn new(max_attempts: u32, window: Duration) -> Self {
360        Self {
361            max_attempts,
362            window,
363            name: None,
364            key_by: None,
365            store: Arc::new(MemoryRateLimitStore::new()),
366        }
367    }
368
369    /// `max_attempts` per minute.
370    pub fn per_minute(max_attempts: u32) -> Self {
371        Self::new(max_attempts, Duration::from_secs(60))
372    }
373
374    /// `max_attempts` per hour.
375    pub fn per_hour(max_attempts: u32) -> Self {
376        Self::new(max_attempts, Duration::from_secs(3600))
377    }
378
379    /// `max_attempts` per day.
380    pub fn per_day(max_attempts: u32) -> Self {
381        Self::new(max_attempts, Duration::from_secs(86_400))
382    }
383
384    /// Count in `store` rather than in this process.
385    ///
386    /// Pass the one bound at boot, so every limiter in the application shares
387    /// a counter and a deployment changes where they all live at once.
388    #[must_use = "this returns a configured throttle rather than configuring in place"]
389    pub fn stored_in(mut self, store: Arc<dyn RateLimitStore>) -> Self {
390        self.store = store;
391        self
392    }
393
394    /// Namespace this limiter's keys.
395    ///
396    /// Two limiters without names, on one route, count the same request twice
397    /// against the same key — so a `5/min` and a `100/hour` become a `5/min`
398    /// that also spends the hourly allowance. A name keeps them apart.
399    ///
400    /// Also what keeps `/login` and `/password/reset` from sharing an
401    /// allowance when both are keyed by email.
402    #[must_use = "this returns a configured throttle rather than configuring in place"]
403    pub fn named(mut self, name: impl Into<String>) -> Self {
404        self.name = Some(name.into());
405        self
406    }
407
408    /// Count against something the request carries.
409    ///
410    /// ```ignore
411    /// ThrottleRequests::per_minute(5).keyed_by(|request| request.input("email"))
412    /// ```
413    ///
414    /// Returning `None` skips the limiter for that request entirely — see
415    /// [`KeyFn`](self).
416    #[must_use = "this returns a configured throttle rather than configuring in place"]
417    pub fn keyed_by<F>(mut self, key: F) -> Self
418    where
419        F: Fn(&Request) -> Option<String> + Send + Sync + 'static,
420    {
421        self.key_by = Some(Arc::new(key));
422        self
423    }
424
425    /// Whether the counters behind this limiter are shared between instances.
426    ///
427    /// `false` means a limit of `n` is really `n × replicas`.
428    pub fn is_shared(&self) -> bool {
429        self.store.is_shared()
430    }
431
432    /// The limit.
433    pub fn max_attempts(&self) -> u32 {
434        self.max_attempts
435    }
436
437    /// Its name, if it has one.
438    pub fn limiter_name(&self) -> Option<&str> {
439        self.name.as_deref()
440    }
441
442    /// The key a request is counted under, namespaced.
443    fn key(&self, request: &Request) -> Option<String> {
444        let key = match &self.key_by {
445            Some(key_by) => key_by(request)?,
446            None => match request.bearer_token() {
447                Some(token) => format!("token:{token}"),
448                None => match request.ip() {
449                    Some(ip) => format!("ip:{ip}"),
450                    None => "anonymous".to_string(),
451                },
452            },
453        };
454
455        Some(match &self.name {
456            Some(name) => format!("throttle:{name}:{key}"),
457            None => format!("throttle:{key}"),
458        })
459    }
460}
461
462impl std::fmt::Debug for ThrottleRequests {
463    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464        f.debug_struct("ThrottleRequests")
465            .field("max_attempts", &self.max_attempts)
466            .field("window", &self.window)
467            .field("name", &self.name)
468            .field("store", &self.store.name())
469            .field("keyed", &if self.key_by.is_some() { "custom" } else { "token-or-ip" })
470            .finish()
471    }
472}
473
474#[async_trait::async_trait]
475impl Middleware for ThrottleRequests {
476    async fn handle(&self, request: Request, next: Next) -> Response {
477        let Some(key) = self.key(&request) else {
478            // Nothing to count against. Not this middleware's problem to
479            // report — a login with no email in it is a validation failure,
480            // and answering `429` here would say something untrue.
481            return next.run(request).await;
482        };
483
484        let hit = match self.store.hit(&key, self.window).await {
485            Ok(hit) => hit,
486            // The counter is unreachable. Fail **open**: a Redis outage that
487            // turned every request into a 429 would be a much larger incident
488            // than the one the limiter exists to prevent, and it would take the
489            // login page down with it.
490            Err(e) => {
491                tracing::error!(
492                    error = %e.message(),
493                    limiter = self.name.as_deref().unwrap_or("unnamed"),
494                    "the rate limit store is unreachable; allowing the request"
495                );
496                return next.run(request).await;
497            }
498        };
499
500        let limit = self.max_attempts.to_string();
501
502        if hit.count > self.max_attempts {
503            let retry_after = hit.resets_in.as_secs() + 1;
504
505            return rainier_support::Error::too_many_requests("Too many requests.")
506                .into_response()
507                .with_header("retry-after", &retry_after.to_string())
508                .with_header("x-ratelimit-limit", &limit)
509                .with_header("x-ratelimit-remaining", "0");
510        }
511
512        let remaining = self.max_attempts - hit.count;
513
514        next.run(request)
515            .await
516            .with_header("x-ratelimit-limit", &limit)
517            .with_header("x-ratelimit-remaining", &remaining.to_string())
518    }
519
520    fn name(&self) -> &'static str {
521        "ThrottleRequests"
522    }
523}
524/// A shared [`ThrottleRequests`], so several routes can be limited against one
525/// counter.
526pub type SharedThrottle = Arc<ThrottleRequests>;
527
528#[cfg(test)]
529mod tests {
530    use super::*;
531    use crate::pipeline::Pipeline;
532    use rainier_http::ClientIp;
533    use serde_json::json;
534
535    async fn echo_input(request: Request) -> Response {
536        Response::json(&request.all())
537    }
538
539    async fn body_json(response: Response) -> Value {
540        let bytes = response.into_http().into_body().collect().await.unwrap();
541        serde_json::from_slice(&bytes).unwrap()
542    }
543
544    #[tokio::test]
545    async fn trim_strings_trims_recursively() {
546        let request = Request::builder()
547            .method(Method::POST)
548            .json(&json!({ "name": "  ada  ", "tags": [" a ", "b"], "nested": { "x": " y " } }))
549            .build();
550
551        let response =
552            Pipeline::new().through(TrimStrings::new()).then(echo_input).run(request).await;
553
554        assert_eq!(
555            body_json(response).await,
556            json!({ "name": "ada", "tags": ["a", "b"], "nested": { "x": "y" } })
557        );
558    }
559
560    #[tokio::test]
561    async fn trim_strings_leaves_passwords_alone() {
562        let request = Request::builder()
563            .method(Method::POST)
564            .json(&json!({ "password": "  spaces matter  ", "email": " a@b.c " }))
565            .build();
566
567        let response =
568            Pipeline::new().through(TrimStrings::new()).then(echo_input).run(request).await;
569
570        assert_eq!(
571            body_json(response).await,
572            json!({ "password": "  spaces matter  ", "email": "a@b.c" })
573        );
574    }
575
576    #[tokio::test]
577    async fn empty_strings_become_null() {
578        let request = Request::builder()
579            .method(Method::POST)
580            .json(&json!({ "nickname": "", "name": "ada", "list": [""] }))
581            .build();
582
583        let response =
584            Pipeline::new().through(ConvertEmptyStringsToNull).then(echo_input).run(request).await;
585
586        assert_eq!(
587            body_json(response).await,
588            json!({ "nickname": null, "name": "ada", "list": [null] })
589        );
590    }
591
592    #[tokio::test]
593    async fn trimming_then_nulling_turns_blank_into_null() {
594        let request =
595            Request::builder().method(Method::POST).json(&json!({ "nickname": "   " })).build();
596
597        let response = Pipeline::new()
598            .through(TrimStrings::new())
599            .through(ConvertEmptyStringsToNull)
600            .then(echo_input)
601            .run(request)
602            .await;
603
604        assert_eq!(body_json(response).await, json!({ "nickname": null }));
605    }
606
607    #[tokio::test]
608    async fn add_headers_decorates_the_response() {
609        let response = Pipeline::new()
610            .through(AddHeaders::security_defaults())
611            .then(|_: Request| async { Response::text("ok") })
612            .run(Request::builder().build())
613            .await;
614
615        assert_eq!(response.header("x-content-type-options"), Some("nosniff"));
616        assert_eq!(response.header("x-frame-options"), Some("SAMEORIGIN"));
617        assert!(response.header("strict-transport-security").is_none());
618    }
619
620    #[tokio::test]
621    async fn cors_answers_a_preflight_without_reaching_the_route() {
622        let request = Request::builder()
623            .method(Method::OPTIONS)
624            .header("origin", "https://app.example")
625            .header("access-control-request-method", "POST")
626            .build();
627
628        let response = Pipeline::new()
629            .through(HandleCors::any_origin())
630            .then(|_: Request| async { panic!("a preflight must not reach the route") })
631            .run(request)
632            .await;
633
634        assert_eq!(response.status(), StatusCode::NO_CONTENT);
635        assert_eq!(response.header("access-control-allow-origin"), Some("*"));
636        assert!(response.header("access-control-allow-methods").unwrap().contains("POST"));
637        assert_eq!(response.header("access-control-max-age"), Some("86400"));
638    }
639
640    #[tokio::test]
641    async fn cors_decorates_an_ordinary_response() {
642        let request = Request::builder().header("origin", "https://app.example").build();
643        let response = Pipeline::new()
644            .through(HandleCors::any_origin())
645            .then(|_: Request| async { Response::text("ok") })
646            .run(request)
647            .await;
648
649        assert_eq!(response.status(), StatusCode::OK);
650        assert_eq!(response.header("access-control-allow-origin"), Some("*"));
651    }
652
653    #[test]
654    fn cors_origin_matching() {
655        let any = HandleCors::any_origin();
656        assert_eq!(any.allowed_origin_for(Some("https://a.example")).as_deref(), Some("*"));
657        assert_eq!(any.allowed_origin_for(None).as_deref(), Some("*"));
658
659        let listed = HandleCors::any_origin().allow_origins(["https://a.example"]);
660        assert_eq!(
661            listed.allowed_origin_for(Some("https://a.example")).as_deref(),
662            Some("https://a.example")
663        );
664        assert_eq!(listed.allowed_origin_for(Some("https://evil.example")), None);
665        assert_eq!(listed.allowed_origin_for(None), None);
666    }
667
668    #[test]
669    fn credentials_never_pair_with_a_wildcard_origin() {
670        let policy = HandleCors::any_origin().allow_credentials(true);
671        // Echoes the origin instead of `*`, which browsers would reject.
672        assert_eq!(
673            policy.allowed_origin_for(Some("https://a.example")).as_deref(),
674            Some("https://a.example")
675        );
676    }
677
678    #[tokio::test]
679    async fn a_restricted_origin_response_varies_on_origin() {
680        let request = Request::builder().header("origin", "https://a.example").build();
681        let response = Pipeline::new()
682            .through(HandleCors::any_origin().allow_origins(["https://a.example"]))
683            .then(|_: Request| async { Response::text("ok") })
684            .run(request)
685            .await;
686
687        assert_eq!(response.header("vary"), Some("Origin"));
688    }
689
690    fn from_ip(n: u8) -> Request {
691        Request::builder().build().with_extension(ClientIp(std::net::IpAddr::from([127, 0, 0, n])))
692    }
693
694    #[tokio::test]
695    async fn throttling_allows_up_to_the_limit_then_rejects() {
696        let throttle: SharedThrottle = Arc::new(ThrottleRequests::per_minute(2));
697
698        for expected_remaining in ["1", "0"] {
699            let response = Pipeline::new()
700                .through_arc(Arc::clone(&throttle) as Arc<dyn Middleware>)
701                .then(|_: Request| async { Response::text("ok") })
702                .run(from_ip(1))
703                .await;
704            assert_eq!(response.status(), StatusCode::OK);
705            assert_eq!(response.header("x-ratelimit-remaining"), Some(expected_remaining));
706        }
707
708        let blocked = Pipeline::new()
709            .through_arc(Arc::clone(&throttle) as Arc<dyn Middleware>)
710            .then(|_: Request| async { panic!("must not reach the route once throttled") })
711            .run(from_ip(1))
712            .await;
713
714        assert_eq!(blocked.status(), StatusCode::TOO_MANY_REQUESTS);
715        assert!(blocked.header("retry-after").is_some());
716    }
717
718    #[tokio::test]
719    async fn throttling_counts_each_client_separately() {
720        let throttle: SharedThrottle = Arc::new(ThrottleRequests::per_minute(1));
721
722        for ip in [1, 2] {
723            let response = Pipeline::new()
724                .through_arc(Arc::clone(&throttle) as Arc<dyn Middleware>)
725                .then(|_: Request| async { Response::text("ok") })
726                .run(from_ip(ip))
727                .await;
728            assert_eq!(response.status(), StatusCode::OK, "ip {ip} should have its own allowance");
729        }
730    }
731
732    #[tokio::test]
733    async fn throttling_keys_authenticated_requests_by_token() {
734        let throttle = ThrottleRequests::per_minute(1);
735
736        let with_token = |token: &str| {
737            Request::builder().header("authorization", &format!("Bearer {token}")).build()
738        };
739
740        assert_eq!(throttle.key(&with_token("a")).as_deref(), Some("throttle:token:a"));
741        assert_ne!(throttle.key(&with_token("a")), throttle.key(&with_token("b")));
742    }
743
744    #[tokio::test]
745    async fn a_name_namespaces_the_key() {
746        // Two limiters on one route would otherwise count the same request
747        // twice against the same key.
748        let plain = ThrottleRequests::per_minute(1);
749        let named = ThrottleRequests::per_minute(1).named("login");
750
751        let request = Request::builder().header("authorization", "Bearer a").build();
752
753        assert_eq!(named.key(&request).as_deref(), Some("throttle:login:token:a"));
754        assert_ne!(plain.key(&request), named.key(&request));
755    }
756
757    #[tokio::test]
758    async fn a_custom_key_counts_what_was_submitted() {
759        let throttle =
760            ThrottleRequests::per_minute(1).named("login").keyed_by(|r| r.input("email"));
761
762        let request = Request::builder()
763            .method(Method::POST)
764            .uri("/login")
765            .json(&json!({ "email": "ada@example.com" }))
766            .build();
767
768        assert_eq!(throttle.key(&request).as_deref(), Some("throttle:login:ada@example.com"));
769    }
770
771    #[tokio::test]
772    async fn a_request_with_nothing_to_key_on_is_not_counted() {
773        // A login with no email is a validation failure, and answering 429
774        // here would say something untrue.
775        let throttle = ThrottleRequests::per_minute(1).keyed_by(|r| r.input("email"));
776        let request = Request::builder().method(Method::POST).uri("/login").build();
777
778        assert_eq!(throttle.key(&request), None);
779
780        // And it reaches the handler however many times it is repeated.
781        for _ in 0..5 {
782            let response = Pipeline::new()
783                .through(ThrottleRequests::per_minute(1).keyed_by(|r| r.input("email")))
784                .then(|_| async { Response::ok("through") })
785                .run(Request::builder().method(Method::POST).uri("/login").build())
786                .await;
787
788            assert_eq!(response.status(), StatusCode::OK);
789        }
790    }
791
792    #[tokio::test]
793    async fn the_limit_is_enforced_and_the_window_resets() {
794        let throttle = Arc::new(ThrottleRequests::new(2, Duration::from_millis(40)));
795
796        let send = || {
797            let throttle = Arc::clone(&throttle);
798            async move {
799                Pipeline::new()
800                    .through_arc(throttle as Arc<dyn Middleware>)
801                    .then(|_| async { Response::ok("through") })
802                    .run(Request::builder().header("authorization", "Bearer a").build())
803                    .await
804            }
805        };
806
807        let first = send().await;
808        assert_eq!(first.status(), StatusCode::OK);
809        assert_eq!(first.header("x-ratelimit-remaining"), Some("1"));
810
811        assert_eq!(send().await.header("x-ratelimit-remaining"), Some("0"));
812
813        let refused = send().await;
814        assert_eq!(refused.status(), StatusCode::TOO_MANY_REQUESTS);
815        assert!(refused.header("retry-after").is_some());
816
817        tokio::time::sleep(Duration::from_millis(60)).await;
818        assert_eq!(send().await.status(), StatusCode::OK, "the window should have rolled over");
819    }
820
821    #[tokio::test]
822    async fn an_unreachable_store_fails_open() {
823        // A Redis outage that turned every request into a 429 would be a much
824        // larger incident than the one the limiter prevents — and it would
825        // take the login page down with it.
826        struct Broken;
827
828        impl RateLimitStore for Broken {
829            fn hit<'a>(
830                &'a self,
831                _key: &'a str,
832                _window: Duration,
833            ) -> rainier_support::BoxFuture<'a, rainier_support::Result<crate::rate_limit::Hit>>
834            {
835                Box::pin(async { Err(rainier_support::Error::internal("connection refused")) })
836            }
837
838            fn peek<'a>(
839                &'a self,
840                _key: &'a str,
841            ) -> rainier_support::BoxFuture<
842                'a,
843                rainier_support::Result<Option<crate::rate_limit::Hit>>,
844            > {
845                Box::pin(async { Ok(None) })
846            }
847
848            fn clear<'a>(
849                &'a self,
850                _key: &'a str,
851            ) -> rainier_support::BoxFuture<'a, rainier_support::Result<()>> {
852                Box::pin(async { Ok(()) })
853            }
854
855            fn is_shared(&self) -> bool {
856                true
857            }
858
859            fn name(&self) -> &str {
860                "broken"
861            }
862        }
863
864        let response = Pipeline::new()
865            .through(ThrottleRequests::per_minute(1).stored_in(Arc::new(Broken)))
866            .then(|_| async { Response::ok("through") })
867            .run(Request::builder().header("authorization", "Bearer a").build())
868            .await;
869
870        assert_eq!(response.status(), StatusCode::OK);
871    }
872
873    #[test]
874    fn a_memory_backed_throttle_says_it_is_not_shared() {
875        assert!(!ThrottleRequests::per_minute(1).is_shared());
876        assert_eq!(ThrottleRequests::per_minute(5).max_attempts(), 5);
877        assert_eq!(ThrottleRequests::per_minute(5).named("x").limiter_name(), Some("x"));
878    }
879
880    #[tokio::test]
881    async fn a_throttled_response_is_a_framework_error_body() {
882        let response = rainier_support::Error::new(
883            rainier_support::ErrorKind::TooManyRequests,
884            "Too many requests.",
885        )
886        .into_response();
887        assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
888    }
889}