FastAPI Custom Middleware and Request Lifecycle

Every request that enters your FastAPI app travels through a defined sequence of steps before your route function runs, and another sequence after it finishes. Understanding this lifecycle lets you intercept and modify requests and responses at exactly the right moment.

The Complete Request Lifecycle

Client sends request
        │
        ▼
1. Uvicorn (web server) receives raw HTTP
        │
        ▼
2. Starlette ASGI layer parses the request
        │
        ▼
3. Middleware stack (outermost first)
   ├── Middleware A (before)
   ├── Middleware B (before)
   └── Middleware C (before)
        │
        ▼
4. Router matches URL to route function
        │
        ▼
5. Dependencies resolved (Depends chain)
        │
        ▼
6. Request body parsed and validated (Pydantic)
        │
        ▼
7. Route function executes
        │
        ▼
8. Response model applied (filter output)
        │
        ▼
9. Middleware stack (innermost first)
   ├── Middleware C (after)
   ├── Middleware B (after)
   └── Middleware A (after)
        │
        ▼
10. Uvicorn sends response to client

Writing a Middleware Class (Starlette Style)

For more complex middleware, inherit from BaseHTTPMiddleware:

from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import Request
import uuid

class RequestIDMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        request_id = str(uuid.uuid4())
        request.state.request_id = request_id   ← attach to request

        response = await call_next(request)

        response.headers["X-Request-ID"] = request_id   ← add to response
        return response

app.add_middleware(RequestIDMiddleware)
Every response now has:
  X-Request-ID: 3f7a9b2e-1234-5678-abcd-ef0123456789

Every route can read it via:
  request.state.request_id

Using request.state to Share Data

request.state is a simple namespace that lives for the duration of one request. Middleware can write to it; route functions and dependencies can read from it:

# Middleware writes:
request.state.start_time = time.time()
request.state.user_country = detect_country(request)

# Route reads:
@app.get("/items")
def get_items(request: Request):
    country = request.state.user_country
    return {"country": country}

Rate Limiting Middleware

from collections import defaultdict
import time

request_counts = defaultdict(list)

class RateLimitMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        ip = request.client.host
        now = time.time()

        # Keep only timestamps from the last 60 seconds
        request_counts[ip] = [t for t in request_counts[ip] if now - t < 60]

        if len(request_counts[ip]) >= 100:
            from starlette.responses import JSONResponse
            return JSONResponse(
                {"error": "Rate limit exceeded: 100 requests per minute"},
                status_code=429
            )

        request_counts[ip].append(now)
        return await call_next(request)
IP 1.2.3.4 sends 100 requests in 60 seconds:
  Requests 1–100  → allowed, proceed to routes
  Request 101     → blocked: 429 Too Many Requests

Middleware Execution Order — Addition vs Execution

app.add_middleware(MiddlewareA)   ← added first
app.add_middleware(MiddlewareB)   ← added second
app.add_middleware(MiddlewareC)   ← added third (outermost)

Request travels:  C → B → A → route
Response travels: route → A → B → C

The last middleware added wraps all the others.

Accessing the Raw Request Object in Routes

Inject Request directly into a route function to read headers, client info, or state set by middleware:

from fastapi import Request

@app.get("/info")
def request_info(request: Request):
    return {
        "client_ip":  request.client.host,
        "method":     request.method,
        "url":        str(request.url),
        "headers":    dict(request.headers),
        "request_id": getattr(request.state, "request_id", None)
    }

Key Points

  • A FastAPI request passes through: server → middleware → router → dependencies → validation → route → middleware → server.
  • Use BaseHTTPMiddleware for class-based middleware with clean separation.
  • Use request.state to pass data from middleware to routes within a single request.
  • The last middleware added runs outermost — it sees the request first and the response last.
  • Inject Request directly into a route function to access raw request data.

Leave a Comment

Your email address will not be published. Required fields are marked *