From 7579dade9ddd2fcc8567ebd848e0da4d8b3f35dd Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:06:18 +0330 Subject: [PATCH] Import source: axon/middleware/rate_limit Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- axon/middleware/rate_limit.py | 39 +++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 axon/middleware/rate_limit.py diff --git a/axon/middleware/rate_limit.py b/axon/middleware/rate_limit.py new file mode 100644 index 0000000..d43e277 --- /dev/null +++ b/axon/middleware/rate_limit.py @@ -0,0 +1,39 @@ +import time +from collections import defaultdict, deque + +from fastapi.responses import JSONResponse +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request + +from axon.config import settings + +_EXEMPT = {"/health", "/metrics", "/docs", "/openapi.json", "/redoc"} + + +class RateLimitMiddleware(BaseHTTPMiddleware): + def __init__(self, app, limit_per_minute: int | None = None) -> None: + super().__init__(app) + self.limit_per_minute = limit_per_minute or settings.rate_limit_per_minute + self._hits: dict[str, deque[float]] = defaultdict(deque) + + async def dispatch(self, request: Request, call_next): + if self.limit_per_minute <= 0 or request.url.path in _EXEMPT: + return await call_next(request) + + key = request.headers.get("X-API-Key") or request.client.host if request.client else "unknown" + now = time.monotonic() + window_start = now - 60.0 + hits = self._hits[key] + + while hits and hits[0] < window_start: + hits.popleft() + + if len(hits) >= self.limit_per_minute: + return JSONResponse( + status_code=429, + content={"detail": "Rate limit exceeded"}, + headers={"Retry-After": "60"}, + ) + + hits.append(now) + return await call_next(request)