Spaces:
Running
Running
Download utils/rate_limiter.py from Success-dev/venthrive-api: direct link, hf CLI and curl.
- Browser
- Download file 4.07 kB
-
https://huggingface.co/spaces/Success-dev/venthrive-api/resolve/main/utils/rate_limiter.py
- Command line
-
hf download hf://spaces/Success-dev/venthrive-api/utils/rate_limiter.py
-
curl -L -o rate_limiter.py https://huggingface.co/spaces/Success-dev/venthrive-api/resolve/main/utils/rate_limiter.py
4.07 kB
| """ | |
| Per-UID rate limiter — design settled 2026-09-09 (see | |
| docs/blueprint/adr/0010-per-uid-in-memory-rate-limiting.md), built 2026-09-25. | |
| Keys by authenticated Firebase UID, not IP — every limited route already | |
| requires a verified token, and a UID can't be spoofed the way an IP/header | |
| can. Counters are in-memory, per-process: safe from client/devtools | |
| tampering by construction, sufficient for the current single-container | |
| Hugging Face Space deployment, and reset on redeploy. If this ever needs to | |
| survive across multiple replicas, move the counters into Firestore/Redis — | |
| the enforce_rate_limit() call site wouldn't need to change. | |
| """ | |
| import time | |
| import threading | |
| from collections import defaultdict | |
| from fastapi import Header, HTTPException | |
| from firebase_admin import firestore | |
| from auth import verify_firebase_token | |
| # (label, limit, period_seconds) per tier. Tiers are named by cost, not by | |
| # route, so multiple routes can share one (see routes/*.py's with_rate_limit | |
| # calls for which tier each route uses). | |
| TIERS: dict[str, list[tuple[str, int, int]]] = { | |
| "chat": [("minute", 10, 60), ("day", 200, 86400)], | |
| "generation": [("hour", 8, 3600), ("day", 30, 86400)], | |
| "transcription": [("day", 20, 86400)], | |
| } | |
| class _Window: | |
| __slots__ = ("count", "reset_at") | |
| def __init__(self) -> None: | |
| self.count = 0 | |
| self.reset_at = 0.0 | |
| _lock = threading.Lock() | |
| _counters: dict[tuple[str, str, str], _Window] = defaultdict(_Window) | |
| # Consecutive-violation count per UID, purely for the "log repeat offenders" | |
| # requirement — never used to make an enforcement decision itself. | |
| _violation_counts: dict[str, int] = defaultdict(int) | |
| def _log_violation(uid: str, tier: str, label: str, count: int, limit: int) -> None: | |
| _violation_counts[uid] += 1 | |
| print( | |
| f"[rate-limit] uid={uid} tier={tier} window={label} " | |
| f"count={count}/{limit} (consecutive violations: {_violation_counts[uid]})" | |
| ) | |
| # Best-effort — a Firestore write failing must never break enforcement itself. | |
| try: | |
| firestore.client().collection("rate_limit_violations").add({ | |
| "uid": uid, | |
| "tier": tier, | |
| "window": label, | |
| "count": count, | |
| "limit": limit, | |
| "consecutiveViolations": _violation_counts[uid], | |
| "at": firestore.SERVER_TIMESTAMP, | |
| }) | |
| except Exception: | |
| pass | |
| def enforce_rate_limit(uid: str, tier: str) -> None: | |
| """Raises HTTPException(429) if uid has exceeded any of tier's limits.""" | |
| if tier not in TIERS: | |
| raise ValueError(f"Unknown rate limit tier: {tier!r}") | |
| now = time.time() | |
| with _lock: | |
| for label, limit, period in TIERS[tier]: | |
| window = _counters[(uid, tier, label)] | |
| if now >= window.reset_at: | |
| window.count = 0 | |
| window.reset_at = now + period | |
| window.count += 1 | |
| if window.count > limit: | |
| retry_after = max(1, int(window.reset_at - now)) | |
| _log_violation(uid, tier, label, window.count, limit) | |
| raise HTTPException( | |
| status_code=429, | |
| detail=( | |
| f"You've hit the {label}ly limit for this action " | |
| f"({limit} per {label}). Try again in {retry_after} seconds." | |
| ), | |
| headers={"Retry-After": str(retry_after)}, | |
| ) | |
| def with_rate_limit(tier: str): | |
| """ | |
| FastAPI dependency: verifies the caller's token AND enforces `tier`'s rate | |
| limit in one pass. Use this in place of `Depends(verify_firebase_token)` | |
| on any route that should be limited — it calls verify_firebase_token | |
| itself rather than composing as a second dependency, so the token is only | |
| verified once per request. | |
| """ | |
| async def dependency(authorization: str = Header(...)) -> dict: | |
| user = await verify_firebase_token(authorization) | |
| enforce_rate_limit(user["uid"], tier) | |
| return user | |
| return dependency | |