Files
fastapi-traffic/docs/user-guide/exception-handling.rst
zanewalker f3453cb0fc release: bump version to 0.3.0
- Refactor Redis backend connection handling and pool management
- Update algorithm implementations with improved type annotations
- Enhance config loader validation with stricter Pydantic schemas
- Improve decorator and middleware error handling
- Expand example scripts with better docstrings and usage patterns
- Add new 00_basic_usage.py example for quick start
- Reorganize examples directory structure
- Fix type annotation inconsistencies across core modules
- Update dependencies in pyproject.toml
2026-03-17 21:04:34 +00:00

278 lines
8.0 KiB
ReStructuredText

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="<h1>Slow down!</h1><p>Please wait a moment before trying again.</p>",
)
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.