Search Topics

Search across all FastAPI topics

GitHub

Middleware

Core Concept

Middleware runs on every request before it reaches your endpoint, and on every response before it's sent back. Perfect for logging, timing, and authentication.

You add logging middleware to track request timing. It logs the request... but the response never comes back. Requests seem to vanish into thin air. Turns out you forgot await call_next(request).

terminal
# Your middleware
@app.middleware("http")
async def log_requests(request: Request, call_next):
    start = time.time()
    print(f"Request: {request.method} {request.url}")
    # Oops... forgot to call call_next(request)
    # The request stops HERE. The endpoint never runs.
    # The client waits... and waits... and eventually times out.

# Client sees:
# curl: (52) Empty reply from server

Question

Why did the request disappear? Because middleware sits between the client and your endpoint. If your middleware doesn't explicitly pass the request forward, it never reaches your route handler. The request just... stops.

The Russian Doll Model

Think of middleware as layers wrapping your endpoint — like Russian dolls. The request passes through each layer going in, hits your endpoint, and then the response travels back out through each layer in reverse.

Client

Sends request

Middleware A

First in, last out

Middleware B

Second in, second out

Middleware C

Last in, first out

Endpoint

Your route handler

The key insight: middleware added last runs first (closest to the endpoint). This LIFO order matters when middleware depends on each other.

The Mental Model

What you just learned

Middleware wraps your endpoint — request flows in, response flows out

call_next(request) is the bridge — skip it and the request stops dead

Middleware runs in reverse registration order (LIFO): last added = closest to endpoint

Creating Middleware

Here's the pattern you'll use 90% of the time. Your middleware does something before the request, calls call_next, then does something with the response.

main.py
import time
from fastapi import FastAPI, Request

app = FastAPI()

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    # BEFORE: runs before your endpoint
    start_time = time.perf_counter()

    response = await call_next(request)  # <-- The magic line

    # AFTER: runs after your endpoint returns
    process_time = time.perf_counter() - start_time
    response.headers["X-Process-Time"] = str(process_time)
    return response  # Don't forget this either!

Insight

Everything before await call_next(request) is your "request phase". Everything after is your "response phase". This single function handles both directions of the request-response cycle.

Middleware Flow Visualized

Watch how a request flows through each middleware layer, hits the endpoint, then the response travels back in reverse order.

Creating Middleware

What you just learned

Use @app.middleware('http') to create request/response middleware

call_next(request) passes the request to the next layer (or endpoint)

Code before call_next is the request phase, code after is the response phase

Built-in CORS Middleware

FastAPI includes built-in CORS middleware so you don't have to write it yourself. You'll use this any time your frontend and backend are on different domains.

main.py
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],  # Your frontend
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Class-Based Middleware

For more complex middleware — where you need configuration or shared state — use the BaseHTTPMiddleware class. Same concept, more structure.

main.py
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import Request
from fastapi.responses import JSONResponse

class AuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        # Check for auth header on every request
        if not request.headers.get("Authorization"):
            # Short-circuit: return early without calling call_next
            return JSONResponse(
                status_code=401,
                content={"detail": "Not authenticated"},
            )
        # Auth header present — let the request through
        response = await call_next(request)
        return response

app.add_middleware(AuthMiddleware)

The Missing call_next

You write middleware to reject requests without an API key. It works for blocked requests, but allowed requests hang forever.

Broken code
main.py
@app.middleware("http")
async def check_api_key(request: Request, call_next):
    api_key = request.headers.get("X-API-Key")
    if not api_key:
        return JSONResponse(
            status_code=403,
            content={"error": "API key required"}
        )
    # Great, API key exists! But... where's call_next?
    # The request with a valid key just hangs here.
    # Nothing happens. No response. Timeout.

Peel the Layers

Middleware wraps your endpoint like Russian dolls. Peel away each layer to see what it does.

Go Deeper: Exception Handling in Middleware

What happens if your endpoint raises an exception? If you don't wrap call_next in a try/except, the exception bubbles up and your "response phase" code never runs.

main.py
@app.middleware("http")
async def error_handling_middleware(request: Request, call_next):
    try:
        response = await call_next(request)
    except Exception as exc:
        # Catch any unhandled exception from the endpoint
        # Log it, return a clean error response
        print(f"Unhandled error: {exc}")
        return JSONResponse(
            status_code=500,
            content={"detail": "Internal server error"},
        )
    return response

Watch out

Be careful with error-handling middleware. If you catch too broadly, you might swallow exceptions that should crash the app (like SystemExit). Only catch Exception, not BaseException.

Advanced Middleware

What you just learned

Class-based middleware with BaseHTTPMiddleware gives you more structure

Middleware can short-circuit — return early without calling call_next

Wrap call_next in try/except to handle endpoint exceptions gracefully

Use middleware for global concerns, dependencies for per-route logic

Think about it...

If you have 3 middleware layers (A, B, C added in that order) and B raises an exception, does C's response code run?

Hint: Think about the order: A wraps B wraps C. Which layer is 'inside' which?

Key Points

Request → Response

Middleware wraps the entire request-response cycle

Execution Order

Middleware runs in reverse order of how it was added

Built-in CORS

CORSMiddleware handles cross-origin requests out of the box

vs Dependencies

Use middleware for global concerns, Depends for per-route logic