Skip to content

Chapter 12 · Middleware and CORS ​

Chapter goals: Understand middleware's position in the request/response lifecycle, learn to write custom middleware, master the execution order rules, and thoroughly understand CORSMiddleware parameters and common pitfalls.

12.1 What Is Middleware ​

Middleware is a function that fires on every request: it sees the request before any path operation, and sees the response after all path operations complete. Typical uses: logging, timing, auth pre-check, compression, CORS.

Workflow:

  1. Receive the request;
  2. Optionally preprocess the request;
  3. Pass the request downstream via call_next(request) (routing → dependency → path operation);
  4. Optionally postprocess the response;
  5. Return the response.

Execution timing detail

The官方 docs explicitly state: for dependencies with yield, their teardown code (the part after yield) runs AFTER middleware; background tasks also run after all middleware. So do not expect to observe "dependency cleanup complete" or "background task done" states inside middleware.

12.2 Custom HTTP Middleware: Request Timing ​

Defined with the @app.middleware("http") decorator. Classic example — add a processing-time header to responses:

python
# 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 call_next = request phase (before entering the path operation)
    start = time.perf_counter()
    response = await call_next(request)
    # After call_next = response phase
    process_time = time.perf_counter() - start
    # Official recommendation: use perf_counter() for higher precision than time.time()
    response.headers["X-Process-Time"] = str(process_time)
    return response


@app.get("/items/")
async def list_items():
    return [{"name": "Foo"}]

Two points to note:

  • call_next is a coroutine; must be awaited;
  • Custom response headers must have an X- prefix to be readable by browser-side JS, and must also be declared in CORS's expose_headers (see 12.5 below).

12.3 Execution Order of Multiple Middlewares ​

This is a high-frequency interview and debugging topic. Each added middleware is like wrapping an additional layer of onion: the last added is the outermost layer.

python
from fastapi import FastAPI, Request

app = FastAPI()


def log_middleware(name: str):
    async def wrap(request: Request, call_next):
        print(f"[{name}] request  ->")
        resp = await call_next(request)
        print(f"[{name}] response <-")
        return resp
    return wrap


app.add_middleware(log_middleware("A"))
app.add_middleware(log_middleware("B"))

Execution order:

text
Request direction:  B → A → route handling
Response direction: route handling → A → B

That is, when add_middleware(MiddlewareB) is called after add_middleware(MiddlewareA), B is the outermost layer, and requests hit B first. This onion model guarantees predictable order — put the middleware that needs to intercept earliest (e.g., HTTPS redirect) on the outermost layer.

12.4 Native ASGI Middleware and Built-in Middleware ​

The decorator approach is essentially a wrapper around Starlette's BaseHTTPMiddleware — convenient but with overhead, and cannot finely control the raw scope. The lower-level approach is to implement an ASGI class middleware, registered via app.add_middleware():

python
from collections.abc import Awaitable, Callable
from typing import Any

from fastapi import FastAPI

app = FastAPI()


class CustomMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        # Request phase
        # ... custom logic ...

        await self.app(scope, receive, send)

        # Response phase
        # ... custom logic ...


app.add_middleware(CustomMiddleware)

12.5 CORS: Cross-Origin Resource Sharing ​

Browsers enforce the same-origin policy: a page on example.com cannot AJAX-call api.example.com unless the latter explicitly allows it. CORS is the mechanism by which the server declares which origins are permitted.

FastAPI provides CORSMiddleware:

python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://frontend.example.com"],  # Specific origin
    # allow_origins=["*"],  # ⚠️ Allows all — insecure for authenticated APIs
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["*"],
    expose_headers=["X-Process-Time", "X-Request-ID"],
    max_age=600,  # Preflight cache duration in seconds
)

Key parameters:

ParameterMeaning
allow_originsList of permitted origins; ["*"] allows all (insecure with credentials)
allow_credentialsWhether to send Cookies with cross-origin requests
allow_methodsHTTP methods allowed; ["*"] allows all
allow_headersHeaders allowed in the request; ["*"] allows all
expose_headersResponse headers the browser's JS is allowed to read
max_ageHow long the browser caches the preflight (OPTIONS) response

Common CORS pitfall

allow_origins=["*"] + allow_credentials=True is invalid — browsers reject this combination. When credentials are needed, specify exact origins.

12.6 Chapter Summary ​

  • Middleware wraps every request/response; call_next(request) passes downstream;
  • Last-added middleware is outermost — execution order is reverse of registration order;
  • @app.middleware("http") decorator for simple middleware; ASGI class for full control;
  • CORSMiddleware with exact origins (never * with credentials); expose_headers for custom headers.

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. What is the execution order of two middlewares A and B added in that order?

2. Why can you not use allow_origins=["*"] with allow_credentials=True?

3. What does expose_headers do in CORSMiddleware?

4. When does the code after call_next in a middleware run?

🛠️ Hands-On Exercises ​

  1. Write a middleware that logs [REQUEST] path method time and [RESPONSE] status_code process_time for every request; verify output in the console.
  2. Add CORSMiddleware allowing https://frontend.example.com with credentials; test with curl -H "Origin: https://frontend.example.com" and inspect the Access-Control-Allow-Origin response header.
  3. Deliberately set allow_origins=["*"] with allow_credentials=True; observe the browser error and fix by listing the exact origin.

Proceed to Chapter 13: Database Integration.