Middleware, CORS, and Trusted Proxies
FastAPI middleware is the code that sees an HTTP request before your path operation runs and sees the response on the way back out. CORS middleware answers a narrower browser question: may JavaScript running on one origin read a response from this API? Trusted proxy configuration answers a deployment question: when a load balancer says the original request used https and came from a certain client IP, should the app believe it?
You should be able to place middleware intentionally, write a CORS policy that matches real browser behavior, and configure forwarded-header trust so generated URLs, redirects, logs, and security checks use the real public request instead of the private hop between proxy and ASGI server.
ASGI Middleware Mechanics
FastAPI is built on Starlette, and Starlette speaks ASGI. An incoming HTTP request appears as an ASGI scope plus receive and send callables. The scope contains items such as type, method, path, scheme, server, headers, client, and sometimes root_path. Middleware is an ASGI application wrapped around another ASGI application. It can inspect or change the scope before calling the inner app, intercept request body messages from receive, or alter response messages sent through send.
The middleware added last is the outermost wrapper and sees the request first. The response then travels back through the wrappers in the opposite direction. That means an outer error-handling or security middleware can observe failures from inner layers, while an inner middleware may never see a request rejected by an outer layer.
FastAPI path operations, dependencies, validation, and exception handlers run inside the middleware stack. Middleware is useful for cross-cutting behavior such as request IDs, compression, CORS, host validation, metrics, tracing, and coarse request rejection. It is a poor place for route-specific business rules because middleware usually sees raw paths and methods, not the already-validated endpoint parameters.
CORS Internals
CORS is enforced by browsers, not by curl, mobile apps, or server-to-server clients. An origin is the tuple of scheme, host, and port. https://admin.example.com and https://api.example.com are different origins, as are http://localhost:3000 and http://localhost:8000. When browser JavaScript calls your API from another origin, the browser sends an Origin header and only exposes the response to JavaScript if the API returns compatible CORS headers.
A simple cross-origin request, such as a basic GET, reaches your endpoint and receives CORS response headers if the origin is allowed. A preflight request is an OPTIONS request sent before the real request when the method or headers are not simple, for example POST with an Authorization header. Starlette’s CORSMiddleware can answer that preflight directly, before the endpoint runs. A successful preflight does not mean the real request is authorized; it only means the browser may send it.
The most common CORS mistake is treating it as authentication. CORS does not stop non-browser clients, does not prove identity, and does not protect an endpoint from direct calls. It controls whether a browser page from another origin can read the response. Authentication, authorization, CSRF protection, and rate limits still belong in the API’s normal security model.
Trusted Proxy Mechanics
Most production FastAPI services are not contacted directly by the public internet. A reverse proxy, ingress controller, CDN, or load balancer terminates TLS and forwards traffic to Uvicorn over a private network. Without extra configuration the ASGI app sees the private hop: perhaps scheme is http, server is 10.0.2.34:8000, and client is the proxy address. That can make generated absolute URLs, OpenAPI server URLs, redirect targets, audit logs, and IP-based controls wrong.
Proxies communicate the original request with forwarded headers: X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host, and sometimes the standardized Forwarded header. These headers are ordinary HTTP headers, so clients can forge them. Uvicorn’s proxy-header handling must therefore be paired with a list of proxy IP addresses allowed to supply them. Trust the proxy that you operate; do not trust arbitrary clients.
TrustedHostMiddleware is related but different. It checks the Host header against allowed hostnames and rejects unexpected hosts. It protects URL generation and host-based routing from host header attacks. It does not by itself decide whether X-Forwarded-For is believable. In a typical deployment you configure forwarded-header trust at the ASGI server layer and host validation in the application middleware stack.
API Anatomy
app.add_middleware(SomeMiddleware, ...) registers middleware on the FastAPI application. For CORS, the key options are allow_origins, allow_origin_regex, allow_methods, allow_headers, allow_credentials, expose_headers, and max_age. Use exact origins when you can. Wildcards are acceptable for truly public, credential-free APIs, but a credentialed browser request needs a specific allowed origin rather than a blanket *.
For proxies, the application code and the server command share responsibility. The application can set root_path when it is mounted under a public prefix such as /api, and it can add TrustedHostMiddleware to reject unknown hosts. The Uvicorn process can enable proxy header interpretation and restrict which upstream IPs are trusted. Your infrastructure should also overwrite, not append to, forwarded headers received from public clients before forwarding to the app.
Example 1: A Minimal Custom Middleware
This first example adds a response header after the endpoint has produced the response. The middleware calls call_next, waits for the inner app, then mutates the headers. A GET /ping returns JSON {"ok": true} and includes x-app-layer: api. The endpoint does not need to know the header exists.
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
class AddHeaderMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
response = await call_next(request)
response.headers["x-app-layer"] = "api"
return response
app = FastAPI()
app.add_middleware(AddHeaderMiddleware)
@app.get("/ping")
async def ping():
return {"ok": True}
This shape is useful for response headers, timing, request IDs, and simple metrics. Avoid reading the request body here unless you need to, because consuming the body in middleware changes how the inner app can read it. If you must inspect bodies, design and test that path carefully.
Example 2: Browser CORS Policy
The next example allows a browser app hosted at https://admin.example.com to call selected methods on the API with an authorization header. A request from https://evil.example receives no matching access-control-allow-origin header, so a browser refuses to expose the response to that page’s JavaScript.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://admin.example.com"],
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["Authorization", "Content-Type"],
)
@app.get("/reports")
async def reports():
return {"items": ["daily", "weekly"]}
Expected behavior is deterministic for the preflight: an allowed origin, method, and header receive a successful OPTIONS response with CORS headers. A disallowed method or origin fails the preflight, so the browser does not send the real request. Server-side clients can still call /reports; CORS is a browser cooperation protocol.
Example 3: Testing The Preflight
Testing CORS through TestClient is valuable because many broken CORS setups look fine when tested only with curl. This example sends the same headers a browser would send before a credentialed API request.
from fastapi.testclient import TestClient
from app import app
client = TestClient(app)
response = client.options(
"/reports",
headers={
"Origin": "https://admin.example.com",
"Access-Control-Request-Method": "POST",
"Access-Control-Request-Headers": "Authorization",
},
)
assert response.status_code == 200
assert response.headers["access-control-allow-origin"] == "https://admin.example.com"
assert "Authorization" in response.headers["access-control-allow-headers"]
The assertions check the response status and the echoed origin. If the code used allow_origins=["*"] with credentials, a browser would not accept the response for credentialed JavaScript even though the API route itself works.
Example 4: Proxies, Hosts, and Public URLs
This command starts Uvicorn so only two private proxy IP addresses may provide forwarded headers. Replace the addresses with the actual addresses of your load balancer, ingress, or local reverse proxy.
uvicorn app:app --proxy-headers --forwarded-allow-ips=10.0.0.10,10.0.0.11
The app below expects to be publicly served below /api and accepts only known hostnames. When a trusted proxy forwards X-Forwarded-Proto: https and Host: api.example.com, request.base_url and url_for reflect the public API location. If an attacker connects directly and sends fake forwarded headers, Uvicorn should ignore them because the remote address is not in the trusted list.
from fastapi import FastAPI, Request
from fastapi.middleware.trustedhost import TrustedHostMiddleware
app = FastAPI(root_path="/api")
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["api.example.com", "localhost", "127.0.0.1"],
)
@app.get("/where-am-i")
async def where_am_i(request: Request):
return {
"base_url": str(request.base_url),
"url_for_self": str(request.url_for("where_am_i")),
"client_host": request.client.host if request.client else None,
}
Expected output from GET /api/where-am-i behind the proxy is a JSON object whose URLs begin with https://api.example.com/api/. In local tests without the proxy, the URLs reflect the local test server instead.
Design Choices and Trade-offs
Put host validation and proxy normalization early enough that later layers see the right request target. Put CORS where it can handle preflight responses consistently. Put metrics or tracing where it observes the failures you care about. Remember that an outer middleware can hide an inner middleware’s behavior by returning a response without calling the inner app.
CORS policy is a balance between developer convenience and browser exposure. A broad development policy such as allowing local frontend ports may be reasonable in a local settings file, but production should name the real origins. Long max_age values reduce preflight traffic but make policy changes slower to take effect in browsers. Allowing credentials is necessary for cookie-based browser auth, but it requires a tighter origin list and a clear CSRF strategy.
Proxy trust is a reliability and security trade-off. If you trust too little, the app sees internal HTTP URLs, redirects users to the wrong scheme, and records the proxy as every client. If you trust too much, clients can spoof IP addresses, schemes, and hosts. The right answer is usually explicit trust for the exact proxy addresses plus infrastructure rules that strip untrusted forwarded headers at the edge.
Failure Modes and Troubleshooting
Symptom: browser console says a request was blocked by CORS, while the endpoint works in curl. Cause: curl is not a browser and does not enforce CORS; the API likely omitted the matching Access-Control-Allow-Origin header or failed the preflight. Diagnose: inspect the browser network panel for the OPTIONS request and compare Origin, requested method, and requested headers with your middleware configuration. Correct: add the exact origin, method, and headers required by the frontend, or remove unnecessary custom headers from the frontend request.
Symptom: login or OAuth callbacks redirect to http:// or to a private hostname. Cause: the app is seeing the proxy-to-app hop instead of the original public scheme and host. Diagnose: add a temporary endpoint that reports request.url, request.base_url, request.client, and the forwarded headers received from the proxy; confirm whether Uvicorn trusts that proxy IP. Correct: enable proxy headers with a restricted trusted IP list and ensure the proxy forwards the original scheme and host.
Symptom: every audit record shows the same client IP. Cause: the application records the load balancer address, or the proxy appends to X-Forwarded-For in a way your stack does not interpret as intended. Diagnose: compare edge logs with app logs for the same request ID. Correct: standardize forwarded-header behavior at the proxy and read client identity only after trusted proxy processing.
Symptom: valid requests receive 400 Invalid host header. Cause: TrustedHostMiddleware allowed hosts do not include the real public hostname, local test host, or health-check host. Diagnose: log the incoming Host header at the proxy and app boundary. Correct: add the intended hostnames, and avoid broad wildcards unless the service truly owns every matching subdomain.
Security, Performance, and Reliability
CORS reduces browser data exposure but should never be your only gate. A private admin API still needs authentication and authorization. Credentialed CORS should be paired with deliberate cookie settings, CSRF protection when cookies are used for state-changing requests, and an origin list that is reviewed when domains change ownership.
Middleware adds latency to every request it wraps. Header logic is usually cheap, but compression, body inspection, synchronous I/O, and remote calls in middleware can affect every endpoint including health checks. Keep middleware nonblocking, bounded, and easy to disable. Prefer deterministic middleware behavior over per-request lookups of policy from an unavailable dependency.
Forwarded headers are security-sensitive input. A spoofed X-Forwarded-Proto: https can affect secure redirects; a spoofed client IP can bypass naive rate limits; a spoofed host can poison generated links. Treat the first trusted proxy as the boundary where public request metadata becomes authoritative.
Hands-on Lab
Prerequisites: a local FastAPI app, Uvicorn, and a browser or HTTP client that can send custom headers. Create a small app using the CORS example and the /where-am-i endpoint. Start it locally without proxy headers first.
- Send
OPTIONS /reportswithOrigin: https://admin.example.com,Access-Control-Request-Method: POST, andAccess-Control-Request-Headers: Authorization. Verify a200response and CORS allow headers. - Repeat the preflight with
Origin: https://other.example.com. Verify the response does not authorize that origin for browser access. - Call
/where-am-ilocally and record the reportedbase_url. It should show your local host and scheme. - Run the app with restricted proxy-header trust using your local proxy address or loopback address in development. Forward a request with
X-Forwarded-Proto: httpsandHost: api.example.com. Verify generated URLs now match the public scheme and host only when the sender is trusted. - Add
TrustedHostMiddlewareand try a request with an unexpectedHost. Verify it is rejected before endpoint code runs.
Verification is complete when allowed preflights pass, disallowed origins do not receive usable browser permissions, public URLs are correct behind the trusted proxy, and unexpected hosts are rejected. Cleanup by removing temporary diagnostic endpoints and reverting development-only origins or trusted loopback settings before committing production configuration.
Assessment Exercises
- A frontend can call
GET /reportsfrom curl but the browser blocks it after anOPTIONSrequest. Identify the likely missing CORS setting and the exact request headers you would inspect. - You add request timing middleware and CORS middleware. Which one should be outermost if you want timing to include preflight responses, and why?
- Your API is behind a load balancer, and password reset links are generated with
http://10.0.0.5:8000. Describe the proxy and application settings you would check. - A teammate proposes
allow_origins=["*"]andallow_credentials=Truefor production. Explain the browser behavior and the safer production alternative. - Design a test that proves a forged forwarded header from an untrusted client does not change the request scheme used by the app.
Summary
FastAPI middleware is an ordered ASGI wrapper around your application. CORS middleware handles browser origin permission, especially preflight requests, but it is not authentication. Trusted proxy configuration tells the server when forwarded request metadata is authoritative, while host validation rejects unexpected hostnames. The dependable setup is explicit ordering, exact browser origins, restricted proxy IP trust, and tests that exercise browser-style preflights and proxy-shaped requests.
