Middleware — Intercepting and Enhancing Requests and Responses
Middleware is like the security checkpoint at an airport—each passenger (request) must pass through customs, security, and the boarding gate in sequence, and at any of these stops, the passenger may be allowed to proceed or stopped, while the return trip (response) follows the reverse order.
1. What You'll Learn
- How Starlette Middleware Works: ASGI Callable Wrapper Chain
- Built-in Middleware:
CORSMiddlewareConfiguration and Security Policies - Custom middleware: request timing, request ID injection, response header injection
- Middleware Execution Order: The Relationship Between Registration Order and Actual Execution Order
- Alice Scenario: Add a request logging middleware and an API rate-limiting middleware to PriceTracker
2. Alice's True Story
(1) Pain Point: The API is subject to malicious traffic spikes and cannot be tracked
After Alice launched her PriceTracker API, she discovered that a certain IP address was sending 5,000 requests per minute, causing the database load to skyrocket. To make matters worse, when Bob's frontend called the API from localhost:3000, the requests were blocked by the browser's CORS policy, causing all requests to fail. Alice needed to resolve both the cross-origin access and request rate-limiting issues simultaneously, but Flask lacks a unified middleware mechanism.
(2) Solutions for FastAPI Middleware
FastAPI/Starlette provides onion-style middleware, with each layer addressing a specific issue: the CORS middleware handles cross-origin requests, the rate-limiting middleware controls request frequency, and the logging middleware records requests—they do not interfere with one another and take effect immediately upon registration.
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["http://localhost:3000"])
(3) Revenue
Bob solved the front-end cross-domain issue with just 5 lines of code. The rate-limiting middleware reduced requests from malicious IPs from 5,000 to 1,000 per minute, and the logging middleware assigned a unique ID to each request for easy tracking.
3. The Onion Model of Middleware
(1) The Onion Model Principle
flowchart TD
Request[Client Request] --> CORS[CORS Middleware]
CORS --> Logging[Logging Middleware]
Logging --> RateLimit[Rate Limit Middleware]
RateLimit --> App[FastAPI Application]
App --> RateLimit2[Rate Limit Response]
RateLimit2 --> Logging2[Logging Response]
Logging2 --> CORS2[CORS Response]
CORS2 --> Response[Client Response]
RateLimit -.->|429 Too Many Requests| Reject[Short-circuit Rejection]
Reject --> Logging2
| Feature | Description |
|---|---|
| Request Direction | From the outside in (the outermost layer registered first) |
| Response Direction | From the inside out (opposite of the request) |
| Short-circuit Capability | Any middleware can return a response early, without proceeding to the next layer |
| Registration Order | The last middleware registered is the first to process the request |
(1) ▶ Example: Middleware Registration Order and Execution Order
from fastapi import FastAPI, Request
import time
app = FastAPI()
# Middleware registered FIRST = outermost layer (executes first on request)
@app.middleware("http")
async def outer_middleware(request: Request, call_next):
print("Outer: before request")
response = await call_next(request)
print("Outer: after response")
return response
# Middleware registered LAST = innermost layer (executes last on request)
@app.middleware("http")
async def inner_middleware(request: Request, call_next):
print("Inner: before request")
response = await call_next(request)
print("Inner: after response")
return response
@app.get("/test")
async def test():
print("Handler: processing")
return {"ok": True}
Output:
Outer: before request
Inner: before request
Handler: processing
Inner: after response
Outer: after response
4. CORS Middleware
(1) Cross-Domain Resource Sharing Configuration
CORS (Cross-Origin Resource Sharing) is a browser security policy that restricts web pages from different origins from accessing APIs. When Bob's front end (localhost:3000) accesses Alice's API (localhost:8000), this constitutes a cross-origin request.
(1) ▶ Example: CORS Configuration for the Development Environment
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"], # Bob's frontend
allow_credentials=True,
allow_methods=["*"], # All HTTP methods
allow_headers=["*"], # All headers
)
Output:
# Execution Successful
(2) ▶ Example: CORS Configuration for a Production Environment
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
app = FastAPI()
ALLOWED_ORIGINS = [
"https://pricetracker.example.com",
"https://admin.pricetracker.example.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
Output:
# Execution Successful
| CORS Settings | Development Environment | Production Environment |
|---|---|---|
allow_origins |
["*"] or localhost |
List of specific domain names |
allow_credentials |
True | True (if cookies are required) |
allow_methods |
["*"] |
Methods Required |
allow_headers |
["*"] |
Only the required headers |
max_age |
Default | 3600 (cache pre-check) |
5. Custom Middleware
(1) Request Timing Middleware
(1) ▶ Example: Logging the processing time for each request
from fastapi import FastAPI, Request
import time
app = FastAPI()
@app.middleware("http")
async def timing_middleware(request: Request, call_next):
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:.4f}s"
return response
@app.get("/products")
async def list_products():
return [{"id": 1, "name": "Widget"}]
Output:
# Function defined successfully
(2) ▶ Example: Request ID Injection Middleware
import uuid
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def request_id_middleware(request: Request, call_next):
request_id = str(uuid.uuid4())
request.state.request_id = request_id # Attach to request state
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
@app.get("/health")
async def health_check(request: Request):
return {
"status": "healthy",
"request_id": request.state.request_id,
}
Output:
# Function defined successfully
(3) ▶ Example: API Rate-Limiting Middleware (Simplified Version)
from fastapi import FastAPI, Request, HTTPException
from collections import defaultdict
import time
app = FastAPI()
# Simple in-memory rate limiter
rate_limits: dict[str, list[float]] = defaultdict(list)
RATE_LIMIT = 1000 # requests per minute
WINDOW = 60 # seconds
@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
client_ip = request.client.host if request.client else "unknown"
now = time.time()
# Clean old entries
rate_limits[client_ip] = [
t for t in rate_limits[client_ip] if now - t < WINDOW
]
# Check limit
if len(rate_limits[client_ip]) >= RATE_LIMIT:
raise HTTPException(
status_code=429,
detail=f"Rate limit exceeded: {RATE_LIMIT} requests per {WINDOW}s",
)
rate_limits[client_ip].append(now)
response = await call_next(request)
response.headers["X-RateLimit-Limit"] = str(RATE_LIMIT)
response.headers["X-RateLimit-Remaining"] = str(
RATE_LIMIT - len(rate_limits[client_ip])
)
return response
Output:
# Function defined successfully
6. Class-based middleware and ASGI callables
(1) How to Write Class-Based Middleware
For more complex middleware logic, we recommend using a class-based approach that directly manipulates ASGI scope, receive, and send.
(1) ▶ Example: Class-based Request Logging Middleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
import logging
import time
logger = logging.getLogger("pricetracker")
class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next) -> Response:
start = time.perf_counter()
logger.info(f"Request: {request.method} {request.url.path}")
response = await call_next(request)
duration = time.perf_counter() - start
logger.info(
f"Response: {request.method} {request.url.path} "
f"status={response.status_code} duration={duration:.4f}s"
)
return response
# Register class-based middleware
app = FastAPI()
app.add_middleware(LoggingMiddleware)
Output:
# Function defined successfully
(2) Comparison of Middleware Types
| Approach | Applicable Scenarios | Advantages | Disadvantages |
|---|---|---|---|
@app.middleware("http") |
Simple interception | Minimal code | Handles HTTP only |
BaseHTTPMiddleware |
Moderate complexity | Configurable | Processes HTTP only |
| Pure ASGI class | Full control | Also handles WebSockets | Complex code |
❓ FAQ
@app.middleware comes before add_middleware (at a higher level). It is recommended to use a consistent approach.allow_origins setting in CORS?allow_credentials=True when * is specified; the browser will reject it.BaseHTTPMiddleware (stream exhaustion after reading the body); for complex scenarios, we recommend using pure ASGI middleware.if settings.ENABLE_RATE_LIMIT: app.add_middleware(RateLimitMiddleware).📖 Summary
- The middleware follows the onion model: requests travel from the outside in, and responses travel from the inside out; any layer can return a response early.
CORSMiddlewareTo resolve cross-domain issues, you must specify a specific domain name in the production environment- Custom middleware can implement cross-cutting concerns such as request timing, request ID injection, and rate limiting.
- The order in which middleware is registered determines the execution order: the last to be registered is the first to process requests (innermost layer)
- Class-based middleware (
BaseHTTPMiddleware) is suitable for complex logic; pure ASGI classes can handle WebSockets
📝 Exercises
- Basic Problem (Difficulty ⭐): Add a CORS middleware to PriceTracker to allow
http://localhost:3000cross-origin access, and verify in a browser that Bob's frontend can call the API. Hint:app.add_middleware(CORSMiddleware, ...) - Advanced Exercise (Difficulty: ⭐⭐): Implement a request timing middleware that adds
X-Process-Timeto the response headers, and use Swagger UI to view the response headers. Hint:@app.middleware("http")+time.perf_counter() - Challenge (Difficulty: ⭐⭐⭐): Implement IP-based rate-limiting middleware (1,000 requests per minute). When the limit is exceeded, return a 429 status code and the
Retry-Afterresponse header, while displaying the remaining quota in the response header. Hint:defaultdict(list)+ time-window cleanup
---|



