FastAPI processes middleware in reverse order of registration: the first middleware added runs outermost (first on request, last on response), and the last middleware added runs innermost (last on request, first on response). SessionMiddleware must be positioned carefully to ensure session data is available when needed.
Middleware Execution Order:
from fastapi import FastAPI
from starlette.middleware.sessions import SessionMiddleware
from starlette.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(TimingMiddleware) # Executes 1st (outermost)
app.add_middleware(CORSMiddleware, ...) # Executes 2nd
app.add_middleware(SessionMiddleware, ...) # Executes 3rd
app.add_middleware(AuthMiddleware) # Executes 4th (innermost)
# Request flow: Timing → CORS → Session → Auth → Route
# Response flow: Auth → Session → CORS → Timing → Client
SessionMiddleware Positioning Rules:
- After CORS Middleware: CORS needs to run before session cookies are set
- Before Authentication Middleware: Auth often needs to read/write session data
- Before Application Middleware: Any middleware that uses
request.session must run after SessionMiddleware
Correct Middleware Stack:
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
import secrets
app = FastAPI()
# Layer 1: CORS (outermost) - handles preflight, sets CORS headers
app.add_middleware(
CORSMiddleware,
allow_origins=["https://example.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Layer 2: Session - provides request.session to inner middleware
app.add_middleware(
SessionMiddleware,
secret_key=secrets.token_hex(32), # Use secure secret in production
session_cookie="session_id",
max_age=1800, # 30 minutes
same_site="lax",
https_only=True # Production only
)
# Layer 3: Custom Auth - can now use request.session
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# ✅ Session available here
user_id = request.session.get("user_id")
if user_id:
request.state.user = await get_user(user_id)
response = await call_next(request)
return response
app.add_middleware(AuthMiddleware)
# Layer 4: Other application middleware (innermost)
class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# ✅ Session and user available here
user_id = request.session.get("user_id")
print(f"Request by user: {user_id}")
response = await call_next(request)
return response
app.add_middleware(LoggingMiddleware)
Why Order Matters for SessionMiddleware:
# ❌ WRONG ORDER - SessionMiddleware after Auth
app.add_middleware(AuthMiddleware) # Tries to access request.session
app.add_middleware(SessionMiddleware, ...) # But session not yet available!
# Result: AttributeError: 'Request' object has no attribute 'session'
# ✅ CORRECT ORDER - SessionMiddleware before Auth
app.add_middleware(SessionMiddleware, ...)
app.add_middleware(AuthMiddleware) # Now request.session is available
Common Middleware Stack Pattern:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from starlette.middleware.sessions import SessionMiddleware
import secrets
app = FastAPI()
# Recommended order (outermost to innermost):
# 1. Trusted Host (security) - outermost
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["example.com", "*.example.com"]
)
# 2. CORS (cross-origin)
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 3. GZip (compression)
app.add_middleware(GZipMiddleware, minimum_size=1000)
# 4. Session (state management)
app.add_middleware(
SessionMiddleware,
secret_key=secrets.token_hex(32)
)
# 5. Custom timing middleware
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start = time.time()
response = await call_next(request)
duration = time.time() - start
response.headers["X-Process-Time"] = str(duration)
return response
app.add_middleware(TimingMiddleware)
# 6. Authentication (business logic) - innermost
app.add_middleware(AuthMiddleware)
OAuth Flow Example:
from authlib.integrations.starlette_client import OAuth
app = FastAPI()
# SessionMiddleware required BEFORE OAuth setup
app.add_middleware(
SessionMiddleware,
secret_key=secrets.token_hex(32)
)
oauth = OAuth()
oauth.register(
name='google',
client_id='...',
client_secret='...',
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
client_kwargs={'scope': 'openid email profile'}
)
@app.get("/login")
async def login(request: Request):
# ✅ request.session available
redirect_uri = request.url_for('auth')
return await oauth.google.authorize_redirect(request, redirect_uri)
@app.get("/auth")
async def auth(request: Request):
# ✅ request.session available for OAuth state verification
token = await oauth.google.authorize_access_token(request)
user = token.get('userinfo')
request.session['user'] = dict(user)
return {"user": user}
SessionMiddleware Configuration:
app.add_middleware(
SessionMiddleware,
secret_key="your-secret-key-min-32-chars", # Required
session_cookie="session", # Cookie name (default: "session")
max_age=1800, # Seconds (default: 2 weeks)
same_site="lax", # "lax", "strict", or "none"
https_only=True, # Production: True, Dev: False
path="/" # Cookie path
)
Debugging Middleware Order:
# Print middleware order
for middleware in app.user_middleware:
print(middleware.cls.__name__)
# Output (innermost to outermost):
# AuthMiddleware
# TimingMiddleware
# SessionMiddleware
# GZipMiddleware
# CORSMiddleware
# TrustedHostMiddleware
Version Note: Middleware ordering behavior consistent since FastAPI 0.40.0+, Starlette 0.13.0+