File size: 1,893 Bytes
e63c592
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from typing import Any

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from slowapi import Limiter
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware
from slowapi.util import get_remote_address

from app.core.config import get_settings
from app.core.logging import get_logger

logger = get_logger(__name__)

# Global limiter instance used for decorators.
limiter = Limiter(key_func=get_remote_address)


def setup_rate_limiter(app: FastAPI) -> None:
    """Configure SlowAPI rate limiting middleware and handlers.

    Limits are enabled/disabled via Settings.RATE_LIMIT_ENABLED.
    """
    settings = get_settings()
    if not getattr(settings, "RATE_LIMIT_ENABLED", True):
        logger.info("Rate limiting is disabled via settings.")
        return

    logger.info("Rate limiting enabled with SlowAPI.")

    app.state.limiter = limiter  # type: ignore[attr-defined]

    @app.exception_handler(RateLimitExceeded)
    async def rate_limit_exceeded_handler(  # type: ignore[no-redef]
        request: Request,
        exc: RateLimitExceeded,
    ) -> JSONResponse:
        retry_after: str | None = None
        try:
            retry_after = exc.headers.get("Retry-After")  # type: ignore[assignment]
        except Exception:  # noqa: BLE001
            retry_after = None

        logger.warning(
            "Rate limit exceeded path=%s client=%s limit=%s",
            request.url.path,
            get_remote_address(request),
            exc.detail,
        )
        content: dict[str, Any] = {
            "detail": "Rate limit exceeded. Please slow down your requests.",
        }
        if retry_after is not None:
            content["retry_after"] = retry_after
        return JSONResponse(status_code=429, content=content)

    # Attach SlowAPI middleware
    app.add_middleware(SlowAPIMiddleware)