Skip to main content

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.

Learning Focus

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:

app/routers/info.py
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

app/main.py
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

/etc/nginx/sites-available/myapp
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

app/routers/debug.py
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:

app/routers/auth.py
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

danger

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

PitfallCause / SymptomFix
Rate limit based on wrong IPProxy headers not trustedAdd ProxyHeadersMiddleware
https redirect loopsNginx sending HTTP to FastAPI, which redirects to HTTPSTrust X-Forwarded-Proto and use redirect_slashes=False
IP spoofing via headertrusted_hosts="*" with public accessRestrict trusted_hosts to the proxy's private IP
Wrong URL in OAuth redirectScheme not trusted from proxyTrust X-Forwarded-Proto to get the real scheme
Multiple proxies inflating IP listX-Forwarded-For has multiple IPsParse the leftmost (first) IP as the real client IP

Hands-On Practice

test-proxy-headers.sh
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"}

What's Next