1use axum::{
2 extract::{ConnectInfo, Request},
3 http::{HeaderValue, StatusCode},
4 middleware::Next,
5 response::{IntoResponse, Response},
6 Json,
7};
8use governor::{
9 clock::DefaultClock,
10 state::{InMemoryState, NotKeyed},
11 Quota, RateLimiter,
12};
13use serde::Serialize;
14use std::collections::HashMap;
15use std::net::{IpAddr, SocketAddr};
16use std::num::NonZeroU32;
17use std::sync::{Arc, Mutex};
18use std::time::Duration;
19use tracing::warn;
20
21type IpRateLimiter = RateLimiter<NotKeyed, InMemoryState, DefaultClock>;
22
23pub(crate) const MAX_LIMITER_ENTRIES: usize = 10_000;
26
27pub struct RateLimitState {
32 pub generation_quota: Quota,
33 pub read_quota: Quota,
34 pub(crate) generation_limiters: Mutex<HashMap<IpAddr, Arc<IpRateLimiter>>>,
35 pub(crate) read_limiters: Mutex<HashMap<IpAddr, Arc<IpRateLimiter>>>,
36}
37
38impl RateLimitState {
39 pub(crate) fn new(generation_quota: Quota, read_quota: Quota) -> Self {
40 Self {
41 generation_quota,
42 read_quota,
43 generation_limiters: Mutex::new(HashMap::new()),
44 read_limiters: Mutex::new(HashMap::new()),
45 }
46 }
47
48 pub(crate) fn get_generation_limiter(&self, ip: IpAddr) -> Arc<IpRateLimiter> {
49 let mut map = self.generation_limiters.lock().unwrap();
50 if map.len() >= MAX_LIMITER_ENTRIES {
51 warn!(
52 entries = map.len(),
53 "generation rate limiter map exceeded cap, evicting all entries"
54 );
55 map.clear();
56 }
57 map.entry(ip)
58 .or_insert_with(|| Arc::new(RateLimiter::direct(self.generation_quota)))
59 .clone()
60 }
61
62 fn get_read_limiter(&self, ip: IpAddr) -> Arc<IpRateLimiter> {
63 let mut map = self.read_limiters.lock().unwrap();
64 if map.len() >= MAX_LIMITER_ENTRIES {
65 warn!(
66 entries = map.len(),
67 "read rate limiter map exceeded cap, evicting all entries"
68 );
69 map.clear();
70 }
71 map.entry(ip)
72 .or_insert_with(|| Arc::new(RateLimiter::direct(self.read_quota)))
73 .clone()
74 }
75}
76
77pub type RateLimitConfig = Option<Arc<RateLimitState>>;
79
80#[derive(Debug, Serialize)]
81struct RateLimitError {
82 error: String,
83 code: String,
84}
85
86pub fn load_rate_limit_config() -> anyhow::Result<RateLimitConfig> {
93 let raw = match std::env::var("MOLD_RATE_LIMIT") {
94 Ok(v) if !v.is_empty() => v,
95 _ => return Ok(None),
96 };
97
98 let (count, period) = parse_rate_spec(&raw)?;
99
100 let burst = match std::env::var("MOLD_RATE_LIMIT_BURST") {
101 Ok(v) if !v.is_empty() => v
102 .parse::<u32>()
103 .map_err(|_| anyhow::anyhow!("MOLD_RATE_LIMIT_BURST must be a positive integer"))?,
104 _ => (count * 2).min(100), };
106
107 let generation_quota = build_quota(count, burst, period)?;
108
109 let read_count = (count * 10).min(1000);
111 let read_burst = (burst * 10).min(1000);
112 let read_quota = build_quota(read_count, read_burst, period)?;
113
114 tracing::info!(
115 rate = %raw,
116 burst,
117 read_multiplier = 10,
118 "rate limiting enabled"
119 );
120
121 Ok(Some(Arc::new(RateLimitState::new(
122 generation_quota,
123 read_quota,
124 ))))
125}
126
127fn parse_rate_spec(spec: &str) -> anyhow::Result<(u32, Duration)> {
128 let parts: Vec<&str> = spec.splitn(2, '/').collect();
129 if parts.len() != 2 {
130 anyhow::bail!("MOLD_RATE_LIMIT must be in the format N/period (e.g., 10/min)");
131 }
132
133 let count: u32 = parts[0]
134 .trim()
135 .parse()
136 .map_err(|_| anyhow::anyhow!("MOLD_RATE_LIMIT count must be a positive integer"))?;
137 if count == 0 {
138 anyhow::bail!("MOLD_RATE_LIMIT count must be greater than 0");
139 }
140
141 let period = match parts[1].trim() {
142 "sec" | "second" | "s" => Duration::from_secs(1),
143 "min" | "minute" | "m" => Duration::from_secs(60),
144 "hour" | "hr" | "h" => Duration::from_secs(3600),
145 other => anyhow::bail!("unknown MOLD_RATE_LIMIT period: {other} (use sec, min, or hour)"),
146 };
147
148 Ok((count, period))
149}
150
151fn build_quota(count: u32, burst: u32, period: Duration) -> anyhow::Result<Quota> {
152 let replenish_interval = period / count;
153 let burst_nz = NonZeroU32::new(burst)
154 .ok_or_else(|| anyhow::anyhow!("rate limit burst must be greater than 0"))?;
155 Ok(Quota::with_period(replenish_interval)
156 .ok_or_else(|| anyhow::anyhow!("invalid rate limit period"))?
157 .allow_burst(burst_nz))
158}
159
160#[derive(Clone, Copy, Debug, PartialEq, Eq)]
162pub enum RouteTier {
163 Generation,
165 Read,
167}
168
169pub fn classify_route(path: &str, method: &axum::http::Method) -> Option<RouteTier> {
171 if path == "/health" || path == "/api/docs" || path == "/api/openapi.json" {
173 return None;
174 }
175
176 match (method.as_str(), path) {
177 ("POST", "/api/generate" | "/api/generate/stream" | "/api/expand") => {
178 Some(RouteTier::Generation)
179 }
180 ("POST", "/api/models/load" | "/api/models/pull") => Some(RouteTier::Generation),
181 ("DELETE", "/api/models/unload") => Some(RouteTier::Generation),
182 ("DELETE", _) if path.starts_with("/api/gallery/") => Some(RouteTier::Generation),
183 ("GET", _) => Some(RouteTier::Read),
184 _ => Some(RouteTier::Read), }
186}
187
188pub async fn rate_limit_middleware(
190 connect_info: ConnectInfo<SocketAddr>,
191 request: Request,
192 next: Next,
193) -> Response {
194 let rl_config = request.extensions().get::<RateLimitConfig>().cloned();
195
196 let state = match rl_config.as_ref().and_then(|c| c.as_ref()) {
197 Some(s) => s,
198 None => return next.run(request).await, };
200
201 let ip = connect_info.0.ip();
202 let tier = classify_route(request.uri().path(), request.method());
203
204 let tier = match tier {
205 Some(t) => t,
206 None => return next.run(request).await, };
208
209 let limiter = match tier {
210 RouteTier::Generation => state.get_generation_limiter(ip),
211 RouteTier::Read => state.get_read_limiter(ip),
212 };
213
214 match limiter.check() {
215 Ok(_) => next.run(request).await,
216 Err(not_until) => {
217 let retry_after = not_until.wait_time_from(governor::clock::Clock::now(
218 &governor::clock::DefaultClock::default(),
219 ));
220 let retry_secs = retry_after.as_secs().max(1);
221 warn!(
222 ip = %ip,
223 retry_after_secs = retry_secs,
224 tier = ?match tier { RouteTier::Generation => "generation", RouteTier::Read => "read" },
225 "rate limit exceeded"
226 );
227 rate_limited_response(retry_secs)
228 }
229 }
230}
231
232fn rate_limited_response(retry_after_secs: u64) -> Response {
233 let body = RateLimitError {
234 error: "rate limit exceeded".to_string(),
235 code: "RATE_LIMITED".to_string(),
236 };
237 let mut response = (StatusCode::TOO_MANY_REQUESTS, Json(body)).into_response();
238 if let Ok(val) = HeaderValue::from_str(&retry_after_secs.to_string()) {
239 response.headers_mut().insert("retry-after", val);
240 }
241 response
242}
243
244pub async fn inject_rate_limit_state(
246 axum::extract::State(rl): axum::extract::State<RateLimitConfig>,
247 mut request: Request,
248 next: Next,
249) -> Response {
250 request.extensions_mut().insert(rl);
251 next.run(request).await
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn parse_rate_per_minute() {
260 let (count, period) = parse_rate_spec("10/min").unwrap();
261 assert_eq!(count, 10);
262 assert_eq!(period, Duration::from_secs(60));
263 }
264
265 #[test]
266 fn parse_rate_per_second() {
267 let (count, period) = parse_rate_spec("5/sec").unwrap();
268 assert_eq!(count, 5);
269 assert_eq!(period, Duration::from_secs(1));
270 }
271
272 #[test]
273 fn parse_rate_per_hour() {
274 let (count, period) = parse_rate_spec("100/hour").unwrap();
275 assert_eq!(count, 100);
276 assert_eq!(period, Duration::from_secs(3600));
277 }
278
279 #[test]
280 fn parse_rate_short_aliases() {
281 assert!(parse_rate_spec("1/s").is_ok());
282 assert!(parse_rate_spec("1/m").is_ok());
283 assert!(parse_rate_spec("1/h").is_ok());
284 }
285
286 #[test]
287 fn parse_rate_invalid_format() {
288 assert!(parse_rate_spec("10").is_err());
289 assert!(parse_rate_spec("10/xyz").is_err());
290 assert!(parse_rate_spec("0/min").is_err());
291 assert!(parse_rate_spec("abc/min").is_err());
292 }
293
294 #[test]
295 fn classify_generation_routes() {
296 use axum::http::Method;
297 assert_eq!(
298 classify_route("/api/generate", &Method::POST),
299 Some(RouteTier::Generation)
300 );
301 assert_eq!(
302 classify_route("/api/generate/stream", &Method::POST),
303 Some(RouteTier::Generation)
304 );
305 assert_eq!(
306 classify_route("/api/expand", &Method::POST),
307 Some(RouteTier::Generation)
308 );
309 assert_eq!(
310 classify_route("/api/models/load", &Method::POST),
311 Some(RouteTier::Generation)
312 );
313 assert_eq!(
314 classify_route("/api/models/pull", &Method::POST),
315 Some(RouteTier::Generation)
316 );
317 assert_eq!(
318 classify_route("/api/models/unload", &Method::DELETE),
319 Some(RouteTier::Generation)
320 );
321 }
322
323 #[test]
324 fn classify_read_routes() {
325 use axum::http::Method;
326 assert_eq!(
327 classify_route("/api/models", &Method::GET),
328 Some(RouteTier::Read)
329 );
330 assert_eq!(
331 classify_route("/api/status", &Method::GET),
332 Some(RouteTier::Read)
333 );
334 assert_eq!(
335 classify_route("/api/gallery", &Method::GET),
336 Some(RouteTier::Read)
337 );
338 }
339
340 #[test]
341 fn classify_exempt_routes() {
342 use axum::http::Method;
343 assert_eq!(classify_route("/health", &Method::GET), None);
344 assert_eq!(classify_route("/api/docs", &Method::GET), None);
345 assert_eq!(classify_route("/api/openapi.json", &Method::GET), None);
346 }
347
348 #[test]
349 fn build_quota_valid() {
350 let q = build_quota(10, 20, Duration::from_secs(60));
351 assert!(q.is_ok());
352 }
353
354 #[test]
355 fn rate_limiter_rejects_after_burst() {
356 let quota = build_quota(1, 2, Duration::from_secs(60)).unwrap();
357 let limiter = RateLimiter::direct(quota);
358 assert!(limiter.check().is_ok()); assert!(limiter.check().is_ok()); assert!(limiter.check().is_err()); }
362}