from __future__ import annotations import time from collections import deque from dataclasses import dataclass from threading import Lock from fastapi import HTTPException, Request, status @dataclass class SlidingWindowRateLimiter: limit: int window_seconds: int def __post_init__(self) -> None: self._events: dict[str, deque[float]] = {} self._lock = Lock() def hit(self, key: str) -> None: now = time.time() floor = now - self.window_seconds with self._lock: bucket = self._events.setdefault(key, deque()) while bucket and bucket[0] <= floor: bucket.popleft() if len(bucket) >= self.limit: raise HTTPException( status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Too many requests. Please try again later.", ) bucket.append(now) def request_client_key(request: Request, *, suffix: str = "") -> str: forwarded_for = request.headers.get("x-forwarded-for", "") client_ip = forwarded_for.split(",", 1)[0].strip() if forwarded_for else (request.client.host if request.client else "unknown") return f"{client_ip}:{suffix}" if suffix else client_ip