mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-20 15:21:30 +02:00
mcp/, toolbox/ and the api's rate limiter each held their own lazy Redis client, three copies of the same lru-singleton pattern reaching into api.core.ratelimit from worker-side code that has no FastAPI app. shared/redis.py is the one place that builds the async and sync clients; ratelimit, throttle, mcp/limits and toolbox/store now import from it. pyproject.toml's isort first-party list gains the packages that have shipped since it was last written (channels, connectors, interest, mcp, reports, stages, toolbox). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VckaDx6mBB4TsG76u61gvv
46 lines
1.5 KiB
Python
46 lines
1.5 KiB
Python
import logging
|
|
import time
|
|
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.requests import Request
|
|
from starlette.responses import JSONResponse
|
|
|
|
from app.config import settings
|
|
from app.core.client_ip import client_id
|
|
from shared.redis import async_client
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_EXEMPT_PREFIXES = ("/api/v1/events", "/api/v1/media")
|
|
|
|
|
|
class GlobalRateLimitMiddleware(BaseHTTPMiddleware):
|
|
async def dispatch(self, request: Request, call_next):
|
|
limit = settings.GLOBAL_RATE_LIMIT_PER_MINUTE
|
|
path = request.url.path
|
|
if (
|
|
limit <= 0
|
|
or request.method == "OPTIONS"
|
|
or any(path.startswith(p) for p in _EXEMPT_PREFIXES)
|
|
):
|
|
return await call_next(request)
|
|
|
|
bucket = int(time.time() // 60)
|
|
key = f"throttle:{client_id(request)}:{bucket}"
|
|
try:
|
|
pipe = async_client().pipeline(transaction=True)
|
|
pipe.incr(key)
|
|
pipe.expire(key, 60, nx=True)
|
|
count, _ = await pipe.execute()
|
|
except Exception as exc:
|
|
logger.warning("global rate limiter unavailable: %s", exc)
|
|
return await call_next(request)
|
|
|
|
if count > limit:
|
|
return JSONResponse(
|
|
status_code=429,
|
|
content={"detail": "Rate limit exceeded. Try again in a minute."},
|
|
headers={"Retry-After": str(60 - int(time.time() % 60))},
|
|
)
|
|
return await call_next(request)
|