Skip to main content

FastAPI Middleware Order

Introduction

When building web applications with FastAPI, middleware components play a crucial role in processing requests and responses. However, the order in which these middleware components are executed can significantly impact your application's behavior. In this guide, we'll explore how FastAPI determines middleware execution order, why it matters, and how you can control it to achieve your desired request processing flow.

Understanding Middleware Execution Flow in FastAPI

In FastAPI, middleware execution follows a specific pattern that is important to understand:

  1. Middleware is executed in the reverse order of registration
  2. For incoming requests, middleware is processed from the outside in (last added → first added)
  3. For outgoing responses, middleware is processed from the inside out (first added → last added)

This flow creates a "nesting doll" pattern that can be visualized as:

Request → Middleware 3 → Middleware 2 → Middleware 1 → Route Handler → Middleware 1 → Middleware 2 → Middleware 3 → Response

Let's see this in action with a practical example.

Basic Example: Visualizing Middleware Execution Order

python
from fastapi import FastAPI, Request
from fastapi.middleware.base import BaseHTTPMiddleware

app = FastAPI()

class SimpleMiddleware(BaseHTTPMiddleware):
def __init__(self, app, name: str):
super().__init__(app)
self.name = name

async def dispatch(self, request: Request, call_next):
print(f"[{self.name}] Before request processing")
response = await call_next(request)
print(f"[{self.name}] After request processing")
return response

# Adding middleware in a specific order
app.add_middleware(SimpleMiddleware, name="Middleware 1")
app.add_middleware(SimpleMiddleware, name="Middleware 2")
app.add_middleware(SimpleMiddleware, name="Middleware 3")

@app.get("/")
async def root():
print("[Route Handler] Processing request")
return {"message": "Hello World"}

When you make a request to the root endpoint, the console output will show:

[Middleware 3] Before request processing
[Middleware 2] Before request processing
[Middleware 1] Before request processing
[Route Handler] Processing request
[Middleware 1] After request processing
[Middleware 2] After request processing
[Middleware 3] After request processing

This clearly demonstrates the reverse order of execution for incoming requests and the sequential order for outgoing responses.

Why Middleware Order Matters

The execution order of middleware is crucial for several reasons:

  1. Dependencies between middleware: One middleware might depend on data processed by another
  2. Authentication and authorization: Security checks should typically happen before processing the request body
  3. Performance optimization: Expensive operations should be placed strategically
  4. Error handling: How errors propagate through your middleware stack

Let's explore a practical example that demonstrates these considerations.

Real-World Example: Authentication, Logging, and Response Modification

python
from fastapi import FastAPI, Request, HTTPException, status
from fastapi.middleware.base import BaseHTTPMiddleware
from fastapi.responses import JSONResponse
import time

app = FastAPI()

class ResponseTimeMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time

# Add processing time header to response
response.headers["X-Process-Time"] = str(process_time)
return response

class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Check for API key in header
api_key = request.headers.get("X-API-Key")
if not api_key or api_key != "valid_key":
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "Invalid or missing API key"}
)

# Add authenticated user info to request state
request.state.user = {"id": "user123", "role": "admin"}
response = await call_next(request)
return response

class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
print(f"[LOG] Incoming request: {request.method} {request.url.path}")

# Try to get user info if authenticated
user_info = getattr(request.state, "user", "unauthenticated")
print(f"[LOG] User: {user_info}")

response = await call_next(request)
print(f"[LOG] Response status: {response.status_code}")
return response

# Important: Order matters here!
app.add_middleware(ResponseTimeMiddleware) # Applied last (outermost)
app.add_middleware(LoggingMiddleware) # Applied second
app.add_middleware(AuthMiddleware) # Applied first (innermost)

@app.get("/protected")
async def protected_route(request: Request):
user = request.state.user
return {"message": f"Hello, {user['id']}!", "role": user["role"]}

In this example, the middleware order is deliberately chosen:

  1. AuthMiddleware is added first but executed third in the request flow

    • It blocks unauthorized requests before they reach the route handler
    • It adds user information to the request state for later use
  2. LoggingMiddleware is added second and executed second

    • It logs information about the request
    • It can access user info from the auth middleware
  3. ResponseTimeMiddleware is added last but executed first

    • It measures the total processing time including all other middleware
    • It adds timing information to the response headers

Controlling Middleware Order in Larger Applications

As your application grows, managing middleware order becomes more complex. Here are some strategies to maintain control:

1. Group middleware by functionality

python
def add_security_middleware(app: FastAPI):
app.add_middleware(AuthMiddleware)
app.add_middleware(RateLimitMiddleware)
# Remember: These are applied in reverse order

def add_monitoring_middleware(app: FastAPI):
app.add_middleware(ResponseTimeMiddleware)
app.add_middleware(LoggingMiddleware)

# Apply in the correct order
def configure_middleware(app: FastAPI):
add_monitoring_middleware(app) # Applied last (outermost layer)
add_security_middleware(app) # Applied first (innermost layer)

2. Use dependency injection for route-specific middleware

For functionality that doesn't need to be applied to every request:

python
from fastapi import Depends

async def verify_admin(request: Request):
user = request.state.user
if user.get("role") != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin access required"
)
return user

@app.get("/admin", dependencies=[Depends(verify_admin)])
async def admin_route():
return {"message": "Admin area"}

3. Use middleware classes with priority attributes

You can extend the middleware system to support priority-based ordering:

python
class PriorityMiddleware(BaseHTTPMiddleware):
priority = 0 # Default priority

@classmethod
def get_priority(cls):
return cls.priority

# Example middlewares with priorities
class SecurityMiddleware(PriorityMiddleware):
priority = 100 # High priority, inner layer

async def dispatch(self, request, call_next):
# Security checks
return await call_next(request)

class CacheMiddleware(PriorityMiddleware):
priority = 50 # Medium priority

async def dispatch(self, request, call_next):
# Caching logic
return await call_next(request)

class LoggingMiddleware(PriorityMiddleware):
priority = 0 # Low priority, outer layer

async def dispatch(self, request, call_next):
# Logging logic
return await call_next(request)

# Add middlewares in any order
def add_priority_middlewares(app: FastAPI):
middlewares = [
LoggingMiddleware,
SecurityMiddleware,
CacheMiddleware
]

# Sort by priority (high to low)
sorted_middlewares = sorted(
middlewares,
key=lambda m: m.get_priority(),
reverse=True
)

# Add in sorted order
for middleware in sorted_middlewares:
app.add_middleware(middleware)

Common Pitfalls and Troubleshooting

When working with middleware order, be aware of these common issues:

  1. CORS middleware position: Always add CORS middleware first (so it's executed last in the request flow)

    python
    from fastapi.middleware.cors import CORSMiddleware

    # Add CORS middleware first (executed last)
    app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
    )
    # Add other middleware after
  2. Authentication before validation: Ensure authentication middleware runs before request validation to avoid unnecessary processing

  3. Error handling: Make sure exceptions from inner middleware are properly caught by outer middleware

  4. Testing middleware individually: Test each middleware in isolation before combining them

Summary

Understanding and controlling middleware execution order in FastAPI is essential for building robust web applications. Key points to remember:

  • Middleware is executed in reverse order of registration
  • For incoming requests, middleware is processed from the outside in
  • For outgoing responses, middleware is processed from the inside out
  • Order matters especially for authentication, logging, and performance monitoring
  • Group middleware by functionality and consider using priority-based ordering for complex applications

By mastering middleware order, you can ensure your FastAPI application processes requests efficiently and securely.

Exercises

  1. Create a middleware setup with three components: one that logs request times, one that validates a custom header, and one that modifies the response. Observe the execution order.

  2. Implement a priority-based middleware system and test it with at least three different middleware components.

  3. Build a middleware that requires authentication for specific URL patterns but not others. Position it correctly relative to other middleware.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)