Back to Blog
Python

Python FastAPI Middleware: CORS and Custom Middleware

python fastapi middleware cors and custom middleware: Learn how to configure CORS and build custom middleware in FastAPI, including execution order, async handling, an...

FastAPIMiddlewareCORSASGIPython
Diagram of a FastAPI request passing through CORS and custom middleware layers before reaching the route handler

python fastapi middleware cors and custom middleware requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you build a FastAPI application, middleware sits between the ASGI server and your route handlers. It can inspect, modify, or reject requests and responses. The two most common uses are enabling CORS and adding cross-cutting logic like logging or authentication. Understanding how FastAPI middleware works helps you avoid subtle bugs around ordering, async execution, and response handling.

Adding CORS Middleware

FastAPI ships with a built-in CORSMiddleware that handles cross-origin resource sharing. You add it via app.add_middleware() and configure allowed origins, methods, and headers.

from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app = FastAPI() app.add_middleware( CORSMiddleware, allow_origins=["https://example.com"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )

The allow_origins list controls which origins may access your API. Setting it to ["*"] disables credential support, so if you need cookies or authorization headers, list specific origins and set allow_credentials=True. The middleware automatically handles preflight OPTIONS requests and adds the appropriate Access-Control-Allow-* headers to responses.

Building Custom Middleware

Custom middleware in FastAPI is an ASGI middleware class. It receives the app (the next ASGI application) and implements __call__ with scope, receive, and send. Here is a minimal logging middleware:

from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request import time class TimingMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): start = time.perf_counter() response = await call_next(request) duration = time.perf_counter() - start response.headers["X-Process-Time"] = str(duration) return response

Add it the same way:

app.add_middleware(TimingMiddleware)

BaseHTTPMiddleware provides a dispatch method that receives a Request and a call_next function. This is the easiest way to write middleware that needs to read the request body or modify the response. For more control over the raw ASGI interface, you can subclass starlette.middleware.base.BaseHTTPMiddleware or write a pure ASGI middleware from scratch.

Middleware Execution Order

The order you call add_middleware matters. Middleware added later is placed closer to the route handler. For example, if you add CORS first and then a custom logging middleware, the logging middleware will run before CORS on the request path and after CORS on the response path. This affects what headers are visible and how errors propagate.

app.add_middleware(CORSMiddleware, ...) # runs outermost app.add_middleware(TimingMiddleware) # runs innermost

In this setup, TimingMiddleware sees the request before CORS modifies it, and the response after CORS has added headers. If you need to log the CORS headers, place the logging middleware outside CORS by adding it first.

Async vs Sync Middleware

BaseHTTPMiddleware supports both async and sync dispatch methods, but sync methods run in a thread pool. This can introduce overhead for simple operations. If your middleware does I/O or uses blocking libraries, prefer async to avoid blocking the event loop. However, note that BaseHTTPMiddleware has a known limitation: it buffers the response body, which can affect streaming responses. For streaming or WebSocket endpoints, you may need a pure ASGI middleware that works directly with send.

Here is a pure ASGI middleware that adds a header without buffering:

class HeaderMiddleware: 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 async def send_wrapper(message): if message["type"] == "http.response.start": message.setdefault("headers", []).append((b"X-Custom", b"value")) await send(message) await self.app(scope, receive, send_wrapper)

Use this pattern when you need to preserve streaming behavior or handle non-HTTP scopes like WebSockets.

Common Pitfalls and Production Considerations

One frequent mistake is setting allow_origins=["*"] together with allow_credentials=True. Browsers reject this combination because wildcard origins cannot be used with credentials. You must list explicit origins when credentials are involved.

Another issue is middleware order affecting exception handling. If a middleware raises an exception, it will not be caught by middleware added earlier in the stack. For example, if your custom middleware calls call_next and the route raises an unhandled exception, the exception propagates up through the middleware stack. If you need to catch exceptions, wrap call_next in a try-except block and handle them appropriately.

In production, consider the performance impact of middleware that reads the entire request body. BaseHTTPMiddleware buffers the body by default, which increases memory usage for large payloads. If you only need headers, use a pure ASGI middleware to avoid buffering. Also, be careful with custom middleware that modifies the response body; it may break content-length headers or streaming.

Finally, remember that middleware runs for every request, including OPTIONS preflight requests. If your custom middleware performs expensive operations, filter out preflight requests early by checking request.method == "OPTIONS" and scope["path"] if needed. This keeps your middleware efficient and avoids unnecessary work.

python fastapi middleware cors and custom middleware: Practi | RYUSLOG DEV