"""Fractafrag — Redis-backed rate limiting middleware.""" import time from fastapi import Request, HTTPException, status from app.redis import get_redis async def check_rate_limit( key: str, max_requests: int, window_seconds: int = 60, ): """ Check and enforce rate limit. Args: key: Unique identifier (e.g., "ip:1.2.3.4" or "user:uuid") max_requests: Maximum requests allowed in the window window_seconds: Time window in seconds Raises: HTTPException 429 if rate limit exceeded """ redis = await get_redis() redis_key = f"ratelimit:{key}" pipe = redis.pipeline() now = time.time() window_start = now - window_seconds # Remove old entries outside the window pipe.zremrangebyscore(redis_key, 0, window_start) # Count current entries pipe.zcard(redis_key) # Add current request pipe.zadd(redis_key, {str(now): now}) # Set TTL on the key pipe.expire(redis_key, window_seconds) results = await pipe.execute() current_count = results[1] if current_count >= max_requests: raise HTTPException( status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail=f"Rate limit exceeded. Max {max_requests} requests per {window_seconds}s.", headers={"Retry-After": str(window_seconds)}, ) async def rate_limit_ip(request: Request, max_requests: int = 100): """Rate limit by IP address. Default: 100 req/min.""" ip = request.client.host if request.client else "unknown" await check_rate_limit(f"ip:{ip}", max_requests) async def rate_limit_user(user_id: str, max_requests: int = 300): """Rate limit by user ID. Default: 300 req/min.""" await check_rate_limit(f"user:{user_id}", max_requests)