bodhi-backend / src /api /ratelimit.py
Bodhi Deploy
Deploy Bodhi backend (API + in-container Redis)
ecb9f70
Raw
History Blame Contribute Delete
2.09 kB
"""Rate limiting via slowapi (global default limit per client IP).
Applied as middleware so all HTTP routes are covered without per-endpoint
decorators. Degrades gracefully: if slowapi isn't installed or is disabled via
env, the app still runs unthrottled (with a warning).
Tuning (env):
RATE_LIMIT_ENABLED — "true"/"false" (default true)
RATE_LIMIT_DEFAULT — e.g. "120/minute" (default)
RATE_LIMIT_STORAGE_URI — e.g. "redis://redis:6379" for multi-worker; the
default "memory://" is per-process only.
"""
from __future__ import annotations
import logging
import os
from fastapi import FastAPI
log = logging.getLogger("bodhi.ratelimit")
def setup_rate_limiting(app: FastAPI) -> None:
enabled = os.getenv("RATE_LIMIT_ENABLED", "true").strip().lower() in (
"1",
"true",
"yes",
)
if not enabled:
log.warning("Rate limiting disabled via RATE_LIMIT_ENABLED.")
return
try:
from slowapi import Limiter
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware
from slowapi.util import get_remote_address
from starlette.responses import JSONResponse
except ImportError:
log.warning("slowapi not installed; rate limiting disabled.")
return
default_limit = os.getenv("RATE_LIMIT_DEFAULT", "120/minute")
storage_uri = os.getenv("RATE_LIMIT_STORAGE_URI", "memory://")
limiter = Limiter(
key_func=get_remote_address,
default_limits=[default_limit],
storage_uri=storage_uri,
headers_enabled=True,
)
app.state.limiter = limiter
async def _handler(request, exc): # noqa: ANN001
return JSONResponse(
status_code=429,
content={"detail": "Rate limit exceeded. Please slow down."},
)
app.add_exception_handler(RateLimitExceeded, _handler)
app.add_middleware(SlowAPIMiddleware)
log.info(
"Rate limiting enabled (default=%s, storage=%s).", default_limit, storage_uri
)