Skip to main content

brainos_httpadapter/
middleware.rs

1//! Tower middleware applied to `/v1/*` routes.
2
3use std::sync::Arc;
4
5use axum::{
6    body::Body,
7    extract::Request,
8    http::StatusCode,
9    middleware::Next,
10    response::{IntoResponse, Response},
11};
12use resilience::RateLimitRegistry;
13
14use crate::auth;
15
16/// Per-client (per-API-key) rate-limit middleware (Issue 51).
17///
18/// Anonymous requests pass through — they hit the auth handler and get a
19/// 401 instead. Authenticated requests check the registry's bucket for
20/// the caller's key and are rejected with HTTP 429 when drained. Detail
21/// about retry-after is intentionally omitted from the public body; the
22/// log line carries the key fingerprint for operators.
23pub async fn rate_limit(
24    registry: Arc<RateLimitRegistry>,
25    request: Request,
26    next: Next,
27) -> Response {
28    if let Some(key) = auth::extract_bearer(request.headers()) {
29        let limiter = registry.get_or_create(key);
30        if !limiter.try_acquire() {
31            let fingerprint = key_fingerprint(key);
32            tracing::warn!(
33                client = %fingerprint,
34                "rate limit exceeded for client"
35            );
36            return (
37                StatusCode::TOO_MANY_REQUESTS,
38                [("content-type", "application/json")],
39                Body::from(
40                    serde_json::json!({
41                        "error": "rate_limited",
42                        "message": "Too many requests",
43                    })
44                    .to_string(),
45                ),
46            )
47                .into_response();
48        }
49    }
50    next.run(request).await
51}
52
53/// Render the first 8 chars of an API key for log correlation without
54/// dumping the full secret.
55fn key_fingerprint(key: &str) -> String {
56    let prefix: String = key.chars().take(8).collect();
57    format!("{prefix}…")
58}