Trust and Proxy Headers
When FastAPI sits behind a reverse proxy (Nginx, Caddy, AWS ALB), the client's real IP and the original protocol (https) are sent in X-Forwarded-For and X-Forwarded-Proto headers. Without trusting these headers, you get wrong IPs for rate limiting and wrong scheme for redirect URLs.
By the end of this lesson you can: enable ProxyHeadersMiddleware for trusted proxies, read the real client IP in routes, and configure Nginx to set the correct forwarding headers.
The Problem
Without proxy header trust:
from fastapi import Request
@router.get("/ip")
async def get_ip(request: Request) -> dict:
return {"ip": request.client.host}
# Returns: 127.0.0.1 (Nginx's IP, not the real client)
ProxyHeadersMiddleware
from fastapi import FastAPI
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
app = FastAPI()
# Trust proxy headers from localhost only
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="127.0.0.1")
# Or trust a specific subnet
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="10.0.0.0/8")
# Or trust all (only safe if no direct public access)
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*")
After this middleware, request.client.host contains the real client IP from X-Forwarded-For.
Nginx Configuration
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:8000;
# Pass real client info to FastAPI
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Reading Headers in Routes
from fastapi import Request, APIRouter
router = APIRouter(prefix="/debug", tags=["debug"])
@router.get("/client-info")
async def client_info(request: Request) -> dict:
return {
"client_host": request.client.host, # Real IP (after proxy trust)
"forwarded_for": request.headers.get("X-Forwarded-For"),
"scheme": request.url.scheme, # https (after proxy trust)
"is_secure": request.url.scheme == "https",
}
Scheme-Aware URLs
After trusting proxy headers, FastAPI correctly generates https:// URLs in redirects:
from fastapi import Request
from fastapi.responses import RedirectResponse
@router.get("/oauth/callback")
async def oauth_callback(request: Request, code: str) -> RedirectResponse:
# This URL will be https:// if the original request was https://
redirect_url = str(request.url_for("dashboard"))
return RedirectResponse(redirect_url)
Security Considerations
If you set trusted_hosts="*", any client can spoof the X-Forwarded-For header by sending it directly, bypassing IP-based rate limits or security checks. Always restrict to the actual proxy IP.
Common Pitfalls
| Pitfall | Cause / Symptom | Fix |
|---|---|---|
| Rate limit based on wrong IP | Proxy headers not trusted | Add ProxyHeadersMiddleware |
https redirect loops | Nginx sending HTTP to FastAPI, which redirects to HTTPS | Trust X-Forwarded-Proto and use redirect_slashes=False |
| IP spoofing via header | trusted_hosts="*" with public access | Restrict trusted_hosts to the proxy's private IP |
| Wrong URL in OAuth redirect | Scheme not trusted from proxy | Trust X-Forwarded-Proto to get the real scheme |
| Multiple proxies inflating IP list | X-Forwarded-For has multiple IPs | Parse the leftmost (first) IP as the real client IP |
Hands-On Practice
uvicorn app.main:app --reload
# Simulate a proxied request with forwarded headers
curl http://localhost:8000/debug/client-info \
-H "X-Forwarded-For: 203.0.113.42" \
-H "X-Forwarded-Proto: https"
# Without ProxyHeadersMiddleware: {"client_host": "127.0.0.1", "scheme": "http"}
# With ProxyHeadersMiddleware: {"client_host": "203.0.113.42", "scheme": "https"}