Skip to main content

throttle_kit/
tower_layer.rs

1use std::fmt;
2use std::future::Future;
3use std::net::IpAddr;
4use std::pin::Pin;
5use std::sync::Arc;
6use std::task::{Context, Poll};
7
8use http::{Extensions, HeaderMap, HeaderValue, StatusCode};
9use tower::Service;
10use tower_layer::Layer;
11
12use crate::RateLimiter;
13use crate::backend::RateLimitBackend;
14use crate::client_ip::{ClientIpConfig, MissingClientIdentity, MissingClientPolicy};
15use crate::quota::Quota;
16
17use std::fmt::Write as _;
18
19/// Fixed-capacity scratch writer over a byte buffer.
20///
21/// `write!` targets here never allocate: the capacity checks use
22/// `get_mut`/`get` (no panics, no `unsafe`), and overflow returns a
23/// formatting error, which callers treat as "keep the empty value" —
24/// unreachable for the inputs used (IP text and decimal integers).
25struct SliceWriter<'a> {
26    buf: &'a mut [u8],
27    len: usize,
28}
29
30impl fmt::Write for SliceWriter<'_> {
31    fn write_str(&mut self, s: &str) -> fmt::Result {
32        let end = self.len + s.len();
33        let slot = self.buf.get_mut(self.len..end).ok_or(fmt::Error)?;
34        slot.copy_from_slice(s.as_bytes());
35        self.len = end;
36        Ok(())
37    }
38}
39
40/// Stack storage for an IP-address rate-limit key: the longest IPv6 text
41/// form is 45 bytes, so keying by client IP needs no heap `String`.
42struct KeyBuf([u8; 45]);
43
44impl KeyBuf {
45    /// Format `ip` into the buffer; the returned `&str` borrows `self`.
46    fn format_ip(&mut self, ip: IpAddr) -> &str {
47        let mut writer = SliceWriter {
48            buf: &mut self.0,
49            len: 0,
50        };
51        // `Display for IpAddr` emits pure ASCII bounded by 45 bytes, so
52        // the write cannot overflow this buffer.
53        let _ = write!(writer, "{ip}");
54        let len = writer.len;
55        // Unreachable fallbacks: `len <= 45` and the contents are ASCII.
56        let bytes = self.0.get(..len).unwrap_or(&[]);
57        // `str::from_utf8` is stable since Rust 1.0; clippy 1.94's
58        // `incompatible_msrv` misfires on it when the package MSRV is
59        // 1.85, so it is suppressed here rather than project-wide.
60        #[allow(clippy::incompatible_msrv)]
61        str::from_utf8(bytes).unwrap_or_default()
62    }
63}
64
65/// Decimal ASCII form of a `u64` in stack space (max 20 digits), for
66/// building `HeaderValue`s without `to_string()` allocations.
67fn u64_header_value(v: u64) -> Option<HeaderValue> {
68    let mut buf = [0u8; 20];
69    let len = {
70        let mut writer = SliceWriter {
71            buf: &mut buf,
72            len: 0,
73        };
74        let _ = write!(writer, "{v}");
75        writer.len
76    };
77    HeaderValue::from_bytes(buf.get(..len)?).ok()
78}
79
80/// Custom, non-IP key extractor for callers that key requests by
81/// something other than client IP (API key, tenant id, …). Receives the
82/// request's headers and extensions; returns the rate-limit key.
83///
84/// Overrides client-IP identity entirely: forwarded headers are never
85/// consulted.
86pub type KeyExtractor = Arc<dyn Fn(&HeaderMap, &Extensions) -> String + Send + Sync>;
87
88/// Where the rate-limit key comes from.
89#[derive(Clone)]
90enum KeySource {
91    /// Client-IP identity resolved through [`ClientIpConfig`] (default).
92    ClientIp {
93        config: ClientIpConfig,
94        missing_policy: MissingClientPolicy,
95    },
96    /// Caller-supplied extractor (API keys, tenant ids, …).
97    Custom(KeyExtractor),
98}
99
100/// Tower layer that applies rate limiting to inner services.
101///
102/// By default the key is the **client's socket address**; forwarded
103/// headers are only believed for peers listed in
104/// [`ClientIpConfig::trusted_proxies`] (see
105/// [`RateLimitLayer::with_client_ip`]). The default is secure by
106/// default: an unconfigured layer ignores `X-Forwarded-For` entirely.
107///
108/// For axum, serve the router with
109/// `.into_make_service_with_connect_info::<SocketAddr>()` so the peer
110/// address is available; without it the
111/// [`MissingClientPolicy`] applies (fail-closed `503` by default).
112#[derive(Clone)]
113pub struct RateLimitLayer<B: RateLimitBackend> {
114    limiter: RateLimiter<B>,
115    // `Arc` so the per-request clone (into the service future) is a
116    // refcount bump — `ClientIpConfig`'s trusted-proxy list would
117    // otherwise be copied on every request.
118    key_source: Arc<KeySource>,
119}
120
121impl<B: RateLimitBackend> RateLimitLayer<B> {
122    /// Create a layer that applies `quota` through the given backend.
123    ///
124    /// Identity defaults to the client's socket address with an empty
125    /// trusted-proxy list (forwarded headers ignored) and
126    /// [`MissingClientPolicy::Reject`] when the peer is unknown.
127    pub fn new(quota: Quota, backend: B) -> Self {
128        Self {
129            limiter: RateLimiter::new(quota, backend),
130            key_source: Arc::new(KeySource::ClientIp {
131                config: ClientIpConfig::default(),
132                missing_policy: MissingClientPolicy::default(),
133            }),
134        }
135    }
136
137    /// Trust `X-Forwarded-For` (or a [`ClientIpConfig::trusted_header`]
138    /// override) only for the given proxy networks, using the
139    /// right-to-left hop walk described there. REQ-THROTTLE-100/101.
140    pub fn with_client_ip(mut self, config: ClientIpConfig) -> Self {
141        let missing_policy = match self.key_source.as_ref() {
142            KeySource::ClientIp { missing_policy, .. } => missing_policy.clone(),
143            KeySource::Custom(_) => MissingClientPolicy::default(),
144        };
145        self.key_source = Arc::new(KeySource::ClientIp {
146            config,
147            missing_policy,
148        });
149        self
150    }
151
152    /// Set the policy for requests whose client identity cannot be
153    /// resolved (no `ConnectInfo` extension). REQ-THROTTLE-103.
154    ///
155    /// Only meaningful for client-IP identity.
156    pub fn with_missing_client_policy(mut self, policy: MissingClientPolicy) -> Self {
157        // Clone the inner source if the `Arc` is shared (the layer was
158        // cloned after construction) so builder mutation never leaks
159        // into the sibling layer.
160        let mut source = match Arc::try_unwrap(self.key_source) {
161            Ok(source) => source,
162            Err(shared) => (*shared).clone(),
163        };
164        if let KeySource::ClientIp { missing_policy, .. } = &mut source {
165            *missing_policy = policy;
166        }
167        self.key_source = Arc::new(source);
168        self
169    }
170
171    /// Key requests by a custom extractor instead of client IP
172    /// (API key, tenant id, …).
173    pub fn with_key_extractor(mut self, extractor: KeyExtractor) -> Self {
174        self.key_source = Arc::new(KeySource::Custom(extractor));
175        self
176    }
177}
178
179impl<S, B> Layer<S> for RateLimitLayer<B>
180where
181    B: RateLimitBackend + Clone,
182{
183    type Service = RateLimitService<S, B>;
184
185    fn layer(&self, inner: S) -> Self::Service {
186        RateLimitService {
187            inner,
188            limiter: self.limiter.clone(),
189            key_source: self.key_source.clone(),
190        }
191    }
192}
193
194/// Tower service that rate-limits requests by client identity.
195///
196/// Identity comes from the layer's key source: client IP (secure
197/// forwarded-header handling, default) or a custom extractor.
198#[derive(Clone)]
199pub struct RateLimitService<S, B: RateLimitBackend> {
200    inner: S,
201    limiter: RateLimiter<B>,
202    key_source: Arc<KeySource>,
203}
204
205impl<S, ReqBody, B> Service<http::Request<ReqBody>> for RateLimitService<S, B>
206where
207    S: Service<http::Request<ReqBody>, Response = http::Response<ReqBody>> + Clone + Send + 'static,
208    S::Future: Send + 'static,
209    B: RateLimitBackend + Clone,
210    // The 429/503 short-circuits must synthesize a response body of the
211    // inner service's body type, so callers must use a `Default` body;
212    // `Send` is required because the request is moved into the future.
213    ReqBody: Default + Send + 'static,
214{
215    type Response = http::Response<ReqBody>;
216    type Error = S::Error;
217    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
218
219    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
220        self.inner.poll_ready(cx)
221    }
222
223    fn call(&mut self, req: http::Request<ReqBody>) -> Self::Future {
224        let key_source = Arc::clone(&self.key_source);
225        let limiter = self.limiter.clone();
226        let mut inner = self.inner.clone();
227
228        Box::pin(async move {
229            // Key scratch space lives in this future's stack frame: the
230            // client-IP key is formatted into `key_buf` (no heap), and
231            // only a custom extractor (or the opt-in fallback key,
232            // borrowed from the `Arc`'d key source) touches heap storage.
233            let mut key_buf = KeyBuf([0u8; 45]);
234            let custom_key: Option<String> = match key_source.as_ref() {
235                KeySource::Custom(extract) => Some(extract(req.headers(), req.extensions())),
236                _ => None,
237            };
238
239            let key: &str = match key_source.as_ref() {
240                KeySource::ClientIp {
241                    config,
242                    missing_policy,
243                } => {
244                    let peer = crate::client_ip::peer_ip_from_extensions(req.extensions());
245                    match crate::client_ip::resolve_client_identity(req.headers(), peer, config) {
246                        Ok(resolved) => key_buf.format_ip(resolved.ip),
247                        Err(MissingClientIdentity) => match missing_policy {
248                            // Fail closed: identity unresolvable → 503.
249                            // REQ-THROTTLE-103.
250                            MissingClientPolicy::Reject => {
251                                let mut response = http::Response::new(ReqBody::default());
252                                *response.status_mut() = StatusCode::SERVICE_UNAVAILABLE;
253                                return Ok(response);
254                            }
255                            MissingClientPolicy::FallbackKey(key) => key.as_ref(),
256                        },
257                    }
258                }
259                KeySource::Custom(_) => custom_key.as_deref().unwrap_or_default(),
260            };
261
262            let result = limiter.check(key).await;
263
264            if !result.allowed {
265                let mut response = http::Response::new(ReqBody::default());
266                *response.status_mut() = StatusCode::TOO_MANY_REQUESTS;
267                response.headers_mut().insert(
268                    http::header::HeaderName::from_static("retry-after"),
269                    http::header::HeaderValue::from_static("1"),
270                );
271                return Ok(response);
272            }
273
274            let mut response = inner.call(req).await?;
275            let headers = response.headers_mut();
276            insert_rate_limit_headers(headers, &result);
277            Ok(response)
278        })
279    }
280}
281
282fn insert_rate_limit_headers(headers: &mut HeaderMap, result: &crate::metrics::RateLimitResult) {
283    // Values are formatted into a stack buffer and validated once by
284    // `HeaderValue::from_bytes` — no `to_string().parse()` round-trips
285    // (each of those costs two allocations per header).
286    if let Some(val) = u64_header_value(result.limit) {
287        headers.insert("X-RateLimit-Limit", val);
288    }
289    if let Some(val) = u64_header_value(result.remaining) {
290        headers.insert("X-RateLimit-Remaining", val);
291    }
292    let reset_secs = result
293        .reset_at
294        .checked_duration_since(std::time::Instant::now())
295        .unwrap_or_default()
296        .as_secs();
297    if let Some(val) = u64_header_value(reset_secs) {
298        headers.insert("X-RateLimit-Reset", val);
299    }
300}
301
302// Tests exercise failure paths and invariants directly; unwrap/expect,
303// slicing, and panicking asserts are acceptable here — violations
304// surface as test failures, not production panics.
305#[allow(
306    clippy::unwrap_used,
307    clippy::expect_used,
308    clippy::indexing_slicing,
309    clippy::panic
310)]
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use crate::metrics::RateLimitResult;
315    use std::convert::Infallible;
316    use std::sync::Arc;
317    use std::time::Instant;
318
319    /// Backend that always allows with a full quota budget.
320    #[derive(Clone)]
321    struct AlwaysAllowBackend;
322
323    impl RateLimitBackend for AlwaysAllowBackend {
324        async fn check(&self, _key: &str, quota: &Quota) -> RateLimitResult {
325            RateLimitResult {
326                allowed: true,
327                remaining: u64::from(quota.burst) - 1,
328                reset_at: Instant::now() + quota.interval(),
329                limit: u64::from(quota.burst),
330                retry_after: None,
331            }
332        }
333    }
334
335    /// Inner service whose `poll_ready` never becomes ready, so readiness
336    /// delegation is observable.
337    #[derive(Clone)]
338    struct PendingService;
339
340    impl Service<http::Request<()>> for PendingService {
341        type Response = http::Response<()>;
342        type Error = Infallible;
343        type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
344
345        fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
346            Poll::Pending
347        }
348
349        fn call(&mut self, _req: http::Request<()>) -> Self::Future {
350            std::future::ready(Ok(http::Response::new(())))
351        }
352    }
353
354    fn layer_service() -> RateLimitService<PendingService, AlwaysAllowBackend> {
355        RateLimitLayer::new(Quota::per_second(10), AlwaysAllowBackend)
356            .with_key_extractor(Arc::new(|_, _| "test-key".to_string()))
357            .layer(PendingService)
358    }
359
360    #[test]
361    fn poll_ready_delegates_to_inner_service() {
362        let mut svc = layer_service();
363        let cx = &mut Context::from_waker(std::task::Waker::noop());
364        assert!(
365            matches!(svc.poll_ready(cx), Poll::Pending),
366            "readiness must come from the inner service"
367        );
368    }
369
370    #[tokio::test]
371    async fn inserts_rate_limit_headers_on_allowed_response() {
372        let mut svc = layer_service();
373        let response = svc
374            .call(http::Request::builder().body(()).unwrap())
375            .await
376            .unwrap();
377
378        assert_eq!(response.status(), StatusCode::OK);
379        let headers = response.headers();
380        assert_eq!(headers.get("X-RateLimit-Limit").unwrap(), "10");
381        assert_eq!(headers.get("X-RateLimit-Remaining").unwrap(), "9");
382        assert!(headers.contains_key("X-RateLimit-Reset"));
383    }
384}