Browse Source

docs: Add advanced middleware examples (timing, request ID, rate limiting)

Adds a new "Custom Middleware Examples" section to docs/en/docs/advanced/middleware.md
covering five production-ready patterns: timing middleware (X-Process-Time header),
request ID middleware (UUID tracing), rate limiting middleware (sliding window per IP),
combining multiple middlewares with ordering notes, and accessing request.state in
route handlers.
pull/15257/head
NEXUS-DEV 3 months ago
parent
commit
7dbbd8cf1e
  1. 196
      docs/en/docs/advanced/middleware.md

196
docs/en/docs/advanced/middleware.md

@ -95,3 +95,199 @@ For example:
* [MessagePack](https://github.com/florimondmanca/msgpack-asgi)
To see other available middlewares check [Starlette's Middleware docs](https://www.starlette.dev/middleware/) and the [ASGI Awesome List](https://github.com/florimondmanca/awesome-asgi).
## Custom Middleware Examples { #custom-middleware-examples }
The following examples demonstrate common middleware patterns used in production FastAPI applications. Each middleware is self-contained and can be added directly to your application.
### Timing Middleware { #timing-middleware }
Track how long each request takes to process. The elapsed time is added as an `X-Process-Time` response header, which is useful for performance monitoring and APM tools.
```python
import time
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
"""
Measure request processing time and expose it via response header.
The header value is in seconds (float), e.g. 0.023 for 23 milliseconds.
Compatible with Prometheus, Datadog, and most APM agents.
"""
start_time = time.perf_counter()
response = await call_next(request)
process_time = time.perf_counter() - start_time
response.headers["X-Process-Time"] = f"{process_time:.6f}"
return response
```
You can read this header in your frontend or monitoring system:
```python
# Client-side example
import httpx
async with httpx.AsyncClient() as client:
r = await client.get("http://localhost:8000/items/")
print(f"Server processing time: {r.headers['x-process-time']}s")
```
### Request ID Middleware { #request-id-middleware }
Assign a unique identifier to every incoming request. This is essential for distributed tracing across microservices and for correlating log entries.
```python
import uuid
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def add_request_id(request: Request, call_next):
"""
Assign a UUID4 request ID to every request.
The ID is available in:
- request.state.request_id (accessible in route handlers and dependencies)
- X-Request-ID response header (returned to the client)
Usage in a route handler:
@app.get("/items/")
async def get_items(request: Request):
rid = request.state.request_id
logger.info("Handling request", extra={"request_id": rid})
"""
request_id = str(uuid.uuid4())
request.state.request_id = request_id
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
```
/// tip
If the client sends an `X-Request-ID` header (e.g., from a load balancer), you can honour it:
```python
request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())
```
///
### Rate Limiting Middleware { #rate-limiting-middleware }
A simple in-memory rate limiter that restricts each IP address to a configurable number of requests per time window. For production use, replace the in-memory store with Redis.
```python
import time
from collections import defaultdict
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
# Configuration
RATE_LIMIT_REQUESTS = 100 # Max requests
RATE_LIMIT_WINDOW = 60 # Per 60 seconds
# In-memory store: {ip: [timestamp, timestamp, ...]}
_request_counts: dict[str, list[float]] = defaultdict(list)
@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
"""
Sliding window rate limiter per client IP address.
Returns HTTP 429 Too Many Requests when the limit is exceeded.
Adds X-RateLimit-* headers to all responses for client awareness.
For production: replace _request_counts with a Redis-backed solution
(e.g., slowapi library or a custom Redis INCR + EXPIRE approach).
"""
client_ip = request.client.host if request.client else "unknown"
now = time.time()
window_start = now - RATE_LIMIT_WINDOW
# Remove timestamps outside the current window
_request_counts[client_ip] = [
ts for ts in _request_counts[client_ip] if ts > window_start
]
request_count = len(_request_counts[client_ip])
if request_count >= RATE_LIMIT_REQUESTS:
return JSONResponse(
status_code=429,
content={"detail": "Too many requests. Please try again later."},
headers={
"X-RateLimit-Limit": str(RATE_LIMIT_REQUESTS),
"X-RateLimit-Remaining": "0",
"Retry-After": str(RATE_LIMIT_WINDOW),
},
)
_request_counts[client_ip].append(now)
response = await call_next(request)
response.headers["X-RateLimit-Limit"] = str(RATE_LIMIT_REQUESTS)
response.headers["X-RateLimit-Remaining"] = str(
RATE_LIMIT_REQUESTS - request_count - 1
)
return response
```
### Combining Multiple Middlewares { #combining-multiple-middlewares }
Middlewares are applied in reverse registration order (last registered = outermost). Here is a recommended ordering:
```python
from fastapi import FastAPI
app = FastAPI()
# Order matters: add_request_id runs first (outermost),
# rate_limit_middleware runs second,
# add_process_time_header runs last (innermost, closest to the route handler).
app.add_middleware(BaseHTTPMiddleware, dispatch=add_process_time_header_dispatch)
app.add_middleware(BaseHTTPMiddleware, dispatch=rate_limit_dispatch)
app.add_middleware(BaseHTTPMiddleware, dispatch=add_request_id_dispatch)
```
/// warning | Performance note
Each `BaseHTTPMiddleware` layer adds a small overhead due to Python async context switching. For high-throughput applications (> 1000 req/s), consider using Starlette's `@app.middleware("http")` decorator or pure ASGI middleware for the most performance-critical layers.
///
### Accessing Middleware State in Route Handlers { #accessing-middleware-state }
State set on `request.state` in middleware is accessible throughout the entire request lifecycle:
```python
from fastapi import FastAPI, Request
import uuid
app = FastAPI()
@app.middleware("http")
async def set_request_context(request: Request, call_next):
request.state.request_id = str(uuid.uuid4())
request.state.user_agent = request.headers.get("user-agent", "unknown")
return await call_next(request)
@app.get("/debug")
async def debug_endpoint(request: Request):
return {
"request_id": request.state.request_id,
"user_agent": request.state.user_agent,
}
```

Loading…
Cancel
Save