ResearchRAG / api /rate_limit.py
riezqidr's picture
feat: enhance production environment settings, improve PDF upload handling, and optimize model loading
5688c6d
Raw
History Blame Contribute Delete
1.99 kB
"""
Rate limiting for the ResearchRAG API.
Every endpoint here is expensive: a chat turn runs HyDE (2 LLM calls), BM25, a
CPU cross-encoder rerank and a streamed completion; an upload runs the full PDF
pipeline. Without limits a single client can exhaust CPU and burn the user's
LLM quota.
Limits are keyed by authenticated user when a bearer token is present, falling
back to client IP for anonymous routes such as login. That way one user hitting
their limit never throttles everyone behind the same NAT.
"""
from __future__ import annotations
from fastapi import Request
from slowapi import Limiter
from slowapi.util import get_remote_address
def _identify(request: Request) -> str:
"""Prefer the token subject; fall back to the peer address."""
auth = request.headers.get("Authorization", "")
if auth.lower().startswith("bearer "):
token = auth[7:].strip()
if token:
try:
from api.security import decode_access_token
return f"user:{decode_access_token(token)}"
except Exception:
# Invalid/expired token — fall through to IP so the limiter
# never rejects a request for the wrong reason.
pass
return f"ip:{get_remote_address(request)}"
# headers_enabled=False: slowapi's X-RateLimit-* injection requires every
# decorated endpoint to accept a `response: Response` parameter, which would
# leak a transport concern into each handler's signature. Enforcement (and the
# 429 response) is unaffected — only the advisory headers are omitted.
# default_limits is what makes the module docstring true. Without it slowapi
# evaluates an EMPTY limit list for every undecorated route, so /api/ready,
# /auth/logout, /auth/me and the whole documents read surface were unlimited —
# each one touching Chroma or SQLite from a finite threadpool.
limiter = Limiter(
key_func=_identify,
default_limits=["120/minute"],
headers_enabled=False,
)