Spaces:
Sleeping
Sleeping
fix: implement rate limiting for analysis endpoints to prevent abuse
Browse files- app/routes/analyze.py +31 -2
app/routes/analyze.py
CHANGED
|
@@ -8,8 +8,9 @@ from __future__ import annotations
|
|
| 8 |
|
| 9 |
import time
|
| 10 |
import uuid
|
|
|
|
| 11 |
from typing import Optional, List, Dict, Any, Literal
|
| 12 |
-
from fastapi import APIRouter, File, UploadFile, Form
|
| 13 |
from pydantic import BaseModel, Field
|
| 14 |
|
| 15 |
from ..services.youtube_analysis import YouTubeAnalysisService
|
|
@@ -18,6 +19,34 @@ from ..services.logging_config import get_logger
|
|
| 18 |
|
| 19 |
router = APIRouter()
|
| 20 |
logger = get_logger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
youtube_service = YouTubeAnalysisService()
|
| 22 |
preview_model = PreviewModel()
|
| 23 |
|
|
@@ -66,7 +95,7 @@ class AnalyzeResponse(BaseModel):
|
|
| 66 |
errors: List[str] = Field(default_factory=list)
|
| 67 |
|
| 68 |
|
| 69 |
-
@router.post("/api/analyze", response_model=AnalyzeResponse)
|
| 70 |
async def analyze(
|
| 71 |
sourceType: str = Form(...),
|
| 72 |
url: Optional[str] = Form(None),
|
|
|
|
| 8 |
|
| 9 |
import time
|
| 10 |
import uuid
|
| 11 |
+
from collections import defaultdict
|
| 12 |
from typing import Optional, List, Dict, Any, Literal
|
| 13 |
+
from fastapi import APIRouter, File, UploadFile, Form, Request, Depends, HTTPException, status
|
| 14 |
from pydantic import BaseModel, Field
|
| 15 |
|
| 16 |
from ..services.youtube_analysis import YouTubeAnalysisService
|
|
|
|
| 19 |
|
| 20 |
router = APIRouter()
|
| 21 |
logger = get_logger(__name__)
|
| 22 |
+
|
| 23 |
+
# Rate limiter for public heavy endpoints
|
| 24 |
+
_analyze_rate_store: dict[str, list[float]] = defaultdict(list)
|
| 25 |
+
_ANALYZE_RATE_WINDOW = 60 # seconds
|
| 26 |
+
_ANALYZE_RATE_MAX = 20 # requests per minute per IP
|
| 27 |
+
_ANALYZE_MAX_IPS = 10_000
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
async def _analyze_rate_limit(request: Request) -> None:
|
| 31 |
+
"""Rate limit for analysis endpoints."""
|
| 32 |
+
client_ip = request.client.host if request.client else "unknown"
|
| 33 |
+
now = time.time()
|
| 34 |
+
cutoff = now - _ANALYZE_RATE_WINDOW
|
| 35 |
+
|
| 36 |
+
# Evict stale IPs when map grows large
|
| 37 |
+
if len(_analyze_rate_store) > _ANALYZE_MAX_IPS:
|
| 38 |
+
stale = [ip for ip, ts in _analyze_rate_store.items() if all(t <= cutoff for t in ts)]
|
| 39 |
+
for ip in stale:
|
| 40 |
+
del _analyze_rate_store[ip]
|
| 41 |
+
|
| 42 |
+
hits = [t for t in _analyze_rate_store[client_ip] if t > cutoff]
|
| 43 |
+
if len(hits) >= _ANALYZE_RATE_MAX:
|
| 44 |
+
raise HTTPException(
|
| 45 |
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
| 46 |
+
detail={"code": "rate_limit_exceeded", "message": "Too many analysis requests. Please wait before trying again."}
|
| 47 |
+
)
|
| 48 |
+
hits.append(now)
|
| 49 |
+
_analyze_rate_store[client_ip] = hits
|
| 50 |
youtube_service = YouTubeAnalysisService()
|
| 51 |
preview_model = PreviewModel()
|
| 52 |
|
|
|
|
| 95 |
errors: List[str] = Field(default_factory=list)
|
| 96 |
|
| 97 |
|
| 98 |
+
@router.post("/api/analyze", response_model=AnalyzeResponse, dependencies=[Depends(_analyze_rate_limit)])
|
| 99 |
async def analyze(
|
| 100 |
sourceType: str = Form(...),
|
| 101 |
url: Optional[str] = Form(None),
|