Home/Learn/FastAPI/Middleware — Code Around Every Request

Middleware — Code Around Every Request

Intermediate
Middleware & CORS

@app.middleware("http") wraps every request: work before call_next, work after — request IDs, timing, access logs. Built-ins cover gzip and host checks; know when a dependency is the better tool.

Overview

Middleware wraps the entire application: every request passes through on the way in, every response on the way out — the natural home for cross-cutting concerns that apply to all routes: request IDs for tracing, timing headers, access logging, compression. FastAPI's @app.middleware("http") gives you the request and a call_next function; everything before the await is inbound, everything after is outbound. Middleware stacks like an onion — added last runs first inbound — and ships with useful built-ins: GZipMiddleware, TrustedHostMiddleware, HTTPSRedirectMiddleware. The design question interviewers probe: middleware vs dependency. Middleware sees every request but knows nothing about which endpoint or user; dependencies are per-route, typed, and overridable in tests. All-routes plumbing → middleware; anything conditional or endpoint-aware → dependency.

Request ID + Timing — the Canonical Middleware

Generate (or propagate) an X-Request-ID, time the handler, log one structured access line, and return both as headers. Every service you build ends up with a version of this exact function.

Before call_next = inbound; after = outbound; state carries context
import logging, time, uuid
from fastapi import FastAPI, Request

app = FastAPI()
log = logging.getLogger("access")

@app.middleware("http")
async def observability(request: Request, call_next):
    # ── inbound: before the route (and its dependencies) run ──
    request_id = request.headers.get("x-request-id", uuid.uuid4().hex[:12])
    request.state.request_id = request_id        # visible to endpoints & handlers
    start = time.perf_counter()

    response = await call_next(request)          # ← the entire app runs here

    # ── outbound: response exists, decorate + log ──
    elapsed_ms = (time.perf_counter() - start) * 1000
    response.headers["X-Request-ID"] = request_id
    response.headers["X-Response-Time"] = f"{elapsed_ms:.1f}ms"
    log.info("%s %s → %d in %.1fms id=%s",
             request.method, request.url.path,
             response.status_code, elapsed_ms, request_id)
    return response

@app.get("/orders/{order_id}")
def get_order(order_id: int, request: Request):
    # the id set by middleware, usable in logs / error reports:
    return {"order": order_id, "trace": request.state.request_id}

# access log:  GET /orders/42 → 200 in 3.2ms id=7f3a99c1e0b2
# The gateway sets X-Request-ID once; every service propagates it →
# one grep traces a user's request across the whole system.

The Stack, the Built-ins, and Middleware vs Dependency

Ordering is an onion: last added, first entered. The built-ins solve solved problems — use them. Then the decision table that keeps concerns in the right layer.

Use the built-ins; keep auth in dependencies, plumbing in middleware
from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware

app = FastAPI()

app.add_middleware(GZipMiddleware, minimum_size=1000)   # compress bodies ≥ 1KB
app.add_middleware(TrustedHostMiddleware,               # reject forged Host headers
                   allowed_hosts=["api.aicancode.org", "localhost"])
# also available: HTTPSRedirectMiddleware (usually the proxy's job instead)

# Onion order — add_middleware PREPENDS:
#   app.add_middleware(A); app.add_middleware(B)
#   request →  B → A → routes → A → B  → response
# Practical order: CORS outermost, then observability, then gzip.

# ── middleware vs dependency ──
#                          middleware            dependency
#   runs for               every route           routes that declare it
#   knows endpoint/user    no                    yes (typed, composable)
#   can be overridden      no                    yes (dependency_overrides)
#   return early?          yes (short-circuit)   yes (raise HTTPException)
#
#   request IDs, gzip, access logs        → middleware
#   auth, rate limits, tenant resolution  → dependencies
#   "auth in middleware" is the classic mistake: it can't vary per route,
#   can't be overridden in tests, and turns /health into a 401.

# Performance note: each @app.middleware("http") wraps the app in
# BaseHTTPMiddleware — fine for a few; for hot-path work (or streaming
# quirks) drop to a pure ASGI middleware. Don't stack fifteen of them.

Key Points to Remember

  • 1@app.middleware("http"): code before await call_next is inbound, after is outbound
  • 2request.state carries per-request context (request_id) to endpoints and handlers
  • 3Onion stacking: added last runs first inbound; CORS goes outermost
  • 4All-routes plumbing → middleware; per-route/testable concerns (auth) → dependencies

Interview Questions

Sign in to ask Aria
1

Implement request tracing across three FastAPI microservices — where is the ID minted and how does it travel?

MediumUber
2

Why is authentication in middleware usually the wrong call in FastAPI?

MediumSwiggy
3

You add middlewares A then B — draw the execution order for one request.

MediumInfosys

Ask Aria about Middleware — Code Around Every Request

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…