- 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
278 lines
8.0 KiB
ReStructuredText
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.
|