Exception Handling ================== When a client exceeds their rate limit, FastAPI Traffic raises a ``RateLimitExceeded`` exception. This guide covers how to handle it gracefully. Default Behavior ---------------- By default, when a rate limit is exceeded, the library raises ``RateLimitExceeded``. FastAPI will convert this to a 500 error unless you handle it. The exception contains useful information: .. code-block:: python from fastapi_traffic import RateLimitExceeded try: # Rate limited operation pass except RateLimitExceeded as exc: print(exc.message) # "Rate limit exceeded" print(exc.retry_after) # Seconds until they can retry (e.g., 45.2) print(exc.limit_info) # RateLimitInfo object with full details Custom Exception Handler ------------------------ The most common approach is to register a custom exception handler: .. code-block:: python from fastapi import FastAPI, Request from fastapi.responses import JSONResponse from fastapi_traffic import RateLimitExceeded app = FastAPI() @app.exception_handler(RateLimitExceeded) async def rate_limit_handler(request: Request, exc: RateLimitExceeded): return JSONResponse( status_code=429, content={ "error": "rate_limit_exceeded", "message": "You're making too many requests. Please slow down.", "retry_after": exc.retry_after, }, headers={ "Retry-After": str(int(exc.retry_after or 60)), }, ) Now clients get a clean JSON response instead of a generic error. Including Rate Limit Headers ---------------------------- The ``limit_info`` object can generate standard rate limit headers: .. code-block:: python @app.exception_handler(RateLimitExceeded) async def rate_limit_handler(request: Request, exc: RateLimitExceeded): headers = {} if exc.limit_info: headers = exc.limit_info.to_headers() return JSONResponse( status_code=429, content={ "error": "rate_limit_exceeded", "retry_after": exc.retry_after, }, headers=headers, ) This adds headers like: .. code-block:: text X-RateLimit-Limit: 100 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1709834400 Retry-After: 45 Different Responses for Different Endpoints ------------------------------------------- You might want different error messages for different parts of your API: .. code-block:: python @app.exception_handler(RateLimitExceeded) async def rate_limit_handler(request: Request, exc: RateLimitExceeded): path = request.url.path if path.startswith("/api/v1/"): # API clients get JSON return JSONResponse( status_code=429, content={"error": "rate_limit_exceeded", "retry_after": exc.retry_after}, ) elif path.startswith("/web/"): # Web users get a friendly HTML page return HTMLResponse( status_code=429, content="
Please wait a moment before trying again.
", ) else: # Default response return JSONResponse( status_code=429, content={"detail": exc.message}, ) Using the on_blocked Callback ----------------------------- Instead of (or in addition to) exception handling, you can use the ``on_blocked`` callback to run code when a request is blocked: .. code-block:: python import logging logger = logging.getLogger(__name__) def log_blocked_request(request: Request, result): """Log when a request is rate limited.""" client_ip = request.client.host if request.client else "unknown" logger.warning( "Rate limit exceeded for %s on %s %s", client_ip, request.method, request.url.path, ) @app.get("/api/data") @rate_limit(100, 60, on_blocked=log_blocked_request) async def get_data(request: Request): return {"data": "here"} The callback receives the request and the rate limit result. It runs before the exception is raised. Exempting Certain Requests -------------------------- Use ``exempt_when`` to skip rate limiting for certain requests: .. code-block:: python def is_admin(request: Request) -> bool: """Check if request is from an admin.""" user = getattr(request.state, "user", None) return user is not None and user.is_admin @app.get("/api/data") @rate_limit(100, 60, exempt_when=is_admin) async def get_data(request: Request): return {"data": "here"} Admin requests bypass rate limiting entirely. Graceful Degradation -------------------- Sometimes you'd rather serve a degraded response than reject the request entirely: .. code-block:: python from fastapi_traffic import RateLimiter, RateLimitConfig from fastapi_traffic.core.limiter import get_limiter @app.get("/api/search") async def search(request: Request, q: str): limiter = get_limiter() config = RateLimitConfig(limit=100, window_size=60) result = await limiter.check(request, config) if not result.allowed: # Return cached/simplified results instead of blocking return { "results": get_cached_results(q), "note": "Results may be stale. Please try again later.", "retry_after": result.info.retry_after, } # Full search return {"results": perform_full_search(q)} Backend Errors -------------- If the rate limit backend fails (Redis down, SQLite locked, etc.), you have options: **Option 1: Fail closed (default)** Requests fail when the backend is unavailable. Safer, but impacts availability. **Option 2: Fail open** Allow requests through when the backend fails: .. code-block:: python @app.get("/api/data") @rate_limit(100, 60, skip_on_error=True) async def get_data(request: Request): return {"data": "here"} **Option 3: Handle the error explicitly** .. code-block:: python from fastapi_traffic import BackendError @app.exception_handler(BackendError) async def backend_error_handler(request: Request, exc: BackendError): # Log the error logger.error("Rate limit backend error: %s", exc.original_error) # Decide what to do # Option A: Allow the request return None # Let the request continue # Option B: Return an error return JSONResponse( status_code=503, content={"error": "service_unavailable"}, ) Other Exceptions ---------------- FastAPI Traffic defines a few exception types: .. code-block:: python from fastapi_traffic import ( RateLimitExceeded, # Rate limit was exceeded BackendError, # Storage backend failed ConfigurationError, # Invalid configuration ) All inherit from ``FastAPITrafficError``: .. code-block:: python from fastapi_traffic.exceptions import FastAPITrafficError @app.exception_handler(FastAPITrafficError) async def traffic_error_handler(request: Request, exc: FastAPITrafficError): """Catch-all for FastAPI Traffic errors.""" if isinstance(exc, RateLimitExceeded): return JSONResponse(status_code=429, content={"error": "rate_limited"}) elif isinstance(exc, BackendError): return JSONResponse(status_code=503, content={"error": "backend_error"}) else: return JSONResponse(status_code=500, content={"error": "internal_error"}) Helper Function --------------- FastAPI Traffic provides a helper to create rate limit responses: .. code-block:: python from fastapi_traffic.core.decorator import create_rate_limit_response @app.exception_handler(RateLimitExceeded) async def rate_limit_handler(request: Request, exc: RateLimitExceeded): return create_rate_limit_response(exc, include_headers=True) This creates a standard 429 response with all the appropriate headers.