404 Not Found

404 Not Found


nginx

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


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.

PYTHON
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

100%
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

PYTHON
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:

TEXT
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

PYTHON
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:

TEXT
# Execution Successful

(2) ▶ Example: CORS Configuration for a Production Environment

PYTHON
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:

TEXT
# 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

PYTHON
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:

TEXT
# Function defined successfully

(2) ▶ Example: Request ID Injection Middleware

PYTHON
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:

TEXT
# Function defined successfully

(3) ▶ Example: API Rate-Limiting Middleware (Simplified Version)

PYTHON
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:

TEXT
# 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

PYTHON
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:

TEXT
# 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

Q Why is the order of middleware registration important?
A The middleware registered last is the first to process requests (innermost layer). CORS should be registered last (outermost layer) to ensure that all responses include CORS headers. Rate limiting should be registered earlier (inner layers) so that rejected requests still pass through CORS.
Q Can @app.middleware and add_middleware be used together?
A Yes, but the middleware registered with @app.middleware comes before add_middleware (at a higher level). It is recommended to use a consistent approach.
Q What solution should be used for rate limiting in production?
A Simple in-memory rate limiting is only suitable for single-process environments. In production, use Redis + SlowAPI or a custom Redis rate-limiting middleware, which supports distributed and multi-process environments.
Q Can I use * for the allow_origins setting in CORS?
A It's acceptable in a development environment, but in a production environment, you must specify a specific domain. You cannot use allow_credentials=True when * is specified; the browser will reject it.
Q Can middleware modify the request body?
A Yes, but it must first read the body and then reconstruct it. There is a known issue with BaseHTTPMiddleware (stream exhaustion after reading the body); for complex scenarios, we recommend using pure ASGI middleware.
Q How do I disable a specific middleware?
A There is no built-in toggle. We recommend using the environment variable: if settings.ENABLE_RATE_LIMIT: app.add_middleware(RateLimitMiddleware).

📖 Summary


📝 Exercises

  1. Basic Problem (Difficulty ⭐): Add a CORS middleware to PriceTracker to allow http://localhost:3000 cross-origin access, and verify in a browser that Bob's frontend can call the API. Hint: app.add_middleware(CORSMiddleware, ...)
  2. Advanced Exercise (Difficulty: ⭐⭐): Implement a request timing middleware that adds X-Process-Time to the response headers, and use Swagger UI to view the response headers. Hint: @app.middleware("http") + time.perf_counter()
  3. 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-After response header, while displaying the remaining quota in the response header. Hint: defaultdict(list) + time-window cleanup

---|

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏