import threading import time from collections import defaultdict from fastapi import Request from app.core.errors import AppError _buckets: dict[str, list[float]] = defaultdict(list) _lock = threading.Lock() def _client_ip(request: Request) -> str: forwarded = request.headers.get("x-forwarded-for") if forwarded: return forwarded.split(",")[0].strip() if request.client and request.client.host: return request.client.host return "unknown" def enforce_rate_limit(request: Request, *, scope: str, limit: int, window_seconds: int) -> None: """In-memory sliding window limiter (per process). Use edge/WAF limits in production at scale.""" import os if os.environ.get("DESI_CLANS_DISABLE_RATE_LIMIT", "").lower() in {"1", "true", "yes"}: return if limit <= 0: return key = f"{scope}:{_client_ip(request)}" now = time.time() with _lock: window = _buckets[key] window[:] = [stamp for stamp in window if now - stamp < window_seconds] if len(window) >= limit: raise AppError("Too many requests. Try again shortly.", status_code=429) window.append(now)