Flask Middleware
Introduction
Middleware is a powerful concept in web development that allows you to execute code before or after a request is processed by your application. In Flask, middleware sits between the web server and your Flask application, intercepting requests and responses to modify or enhance them.
Think of middleware as a series of layers your request passes through before reaching your view function, and then again on the way back out. This makes middleware perfect for cross-cutting concerns like:
- Logging requests and responses
- Authentication and authorization
- CORS (Cross-Origin Resource Sharing) handling
- Request preprocessing
- Response modification
- Performance monitoring
In this tutorial, we'll explore how middleware works in Flask, how to use built-in middleware, and how to create your own custom middleware components.
Understanding Flask Middleware Architecture
Unlike frameworks like Express.js or Django that have dedicated middleware systems, Flask has a slightly different approach based on WSGI (Web Server Gateway Interface) middleware. Flask's middleware system is primarily implemented using:
- WSGI Middleware - Works at the WSGI application level
- Flask's
before_request
andafter_request
hooks - Functions that run before/after each request - Request/Response processors - Functions that modify requests/responses
Let's explore each approach and see how they can be used in your Flask applications.
Flask Request Hooks
Flask's request hooks are the simplest form of middleware-like functionality. They allow you to execute functions before or after a request is processed.
Common Request Hooks
before_request
- Runs before each requestafter_request
- Runs after each request (if no exceptions occurred)teardown_request
- Runs after each request (even if exceptions occurred)before_first_request
- Runs only before the first request to the application
Example: Request Timing with Hooks
Let's create a simple middleware-like functionality using Flask hooks to measure the time each request takes:
import time
from flask import Flask, request, g
app = Flask(__name__)
@app.before_request
def start_timer():
"""Start a timer before each request"""
g.start_time = time.time()
@app.after_request
def log_request_time(response):
"""Log the time taken for each request"""
if hasattr(g, 'start_time'):
elapsed_time = time.time() - g.start_time
print(f"Request to {request.path} took {elapsed_time:.5f} seconds")
return response
@app.route('/')
def home():
return "Welcome to Flask Middleware Tutorial!"
@app.route('/slow')
def slow_route():
# Simulate a slow operation
time.sleep(2)
return "This was a slow request!"
When you run this application and make requests, you'll see output like:
Request to / took 0.00120 seconds
Request to /slow took 2.00345 seconds
This simple middleware logs the time each route takes to process, which can help identify performance bottlenecks in your application.
WSGI Middleware in Flask
WSGI middleware operates at a lower level than Flask hooks. It wraps the entire Flask application and can modify both the request environment and the response. This approach is more powerful but also more complex.
Example: Custom WSGI Middleware
Here's a simple WSGI middleware that adds a custom header to every response:
from flask import Flask
app = Flask(__name__)
class CustomHeaderMiddleware:
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
def custom_start_response(status, headers, exc_info=None):
# Add custom header to all responses
headers.append(('X-Custom-Header', 'Flask-Middleware-Tutorial'))
return start_response(status, headers, exc_info)
# Call the wrapped application with our custom start_response
return self.app(environ, custom_start_response)
# Wrap the Flask app with our middleware
app.wsgi_app = CustomHeaderMiddleware(app.wsgi_app)
@app.route('/')
def home():
return "Check your response headers!"
This middleware adds an X-Custom-Header
to every response from your Flask application.
Application Factory Pattern with Middleware
When using Flask's application factory pattern, you can integrate middleware in a clean, modular way:
from flask import Flask
def create_app(config=None):
app = Flask(__name__)
# Apply configuration
if config:
app.config.update(config)
# Register middleware
register_middleware(app)
# Register blueprints and routes
from .routes import register_routes
register_routes(app)
return app
def register_middleware(app):
# Register WSGI middleware
app.wsgi_app = CustomHeaderMiddleware(app.wsgi_app)
# Register request hooks
@app.before_request
def before_request_func():
# Middleware logic here
pass
@app.after_request
def after_request_func(response):
# Middleware logic here
return response
This approach keeps your middleware organization clean and maintainable as your application grows.
Practical Examples of Flask Middleware
Let's explore some practical examples of middleware you might use in real-world Flask applications.
1. Authentication Middleware
This middleware checks for authentication on protected routes:
from flask import Flask, request, g, redirect, url_for
from functools import wraps
app = Flask(__name__)
# Mock user database (replace with actual database in production)
USERS = {
'api_key_123': {'username': 'user1', 'role': 'admin'},
'api_key_456': {'username': 'user2', 'role': 'user'}
}
@app.before_request
def authenticate():
"""Authenticate users based on API key header"""
# Skip authentication for public endpoints
if request.path == '/' or request.path == '/login':
return
# Check for API key in headers
api_key = request.headers.get('X-API-Key')
if not api_key or api_key not in USERS:
return redirect(url_for('login'))
# Store user info in g for access in route handlers
g.user = USERS[api_key]
@app.route('/')
def home():
return "Welcome to the public homepage!"
@app.route('/login')
def login():
return "Please log in with your API key."
@app.route('/dashboard')
def dashboard():
# This will only be accessible after authentication
return f"Welcome to your dashboard, {g.user['username']}!"
2. Request Logging Middleware
This middleware logs details about each request:
import time
import uuid
from flask import Flask, request, g
import logging
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@app.before_request
def log_request_info():
"""Log information about each request"""
g.request_id = str(uuid.uuid4())
g.start_time = time.time()
logger.info(f"Request {g.request_id}: {request.method} {request.path} - IP: {request.remote_addr}")
# Log request data if appropriate
if request.method in ['POST', 'PUT', 'PATCH'] and request.is_json:
logger.debug(f"Request {g.request_id} data: {request.get_json()}")
@app.after_request
def log_response_info(response):
"""Log information about the response"""
if hasattr(g, 'request_id') and hasattr(g, 'start_time'):
duration = time.time() - g.start_time
logger.info(f"Response {g.request_id}: Status {response.status_code} - Took {duration:.4f}s")
return response
@app.route('/')
def home():
return "Check your logs!"
@app.route('/api/data', methods=['POST'])
def create_data():
return {"success": True, "message": "Data created"}
This middleware will produce logs like:
INFO:__main__:Request 8f3d7e2c-4f91-4b12-8a5e-bb7e1d21f282: GET / - IP: 127.0.0.1
INFO:__main__:Response 8f3d7e2c-4f91-4b12-8a5e-bb7e1d21f282: Status 200 - Took 0.0023s
INFO:__main__:Request a7e6f5d4-3c82-4b91-9a70-12de34f56789: POST /api/data - IP: 127.0.0.1
DEBUG:__main__:Request a7e6f5d4-3c82-4b91-9a70-12de34f56789 data: {'name': 'test', 'value': 123}
INFO:__main__:Response a7e6f5d4-3c82-4b91-9a70-12de34f56789: Status 200 - Took 0.0042s
3. CORS Middleware
While Flask has the flask-cors
extension, you can implement a simple CORS middleware for educational purposes:
from flask import Flask, request, make_response
app = Flask(__name__)
@app.after_request
def add_cors_headers(response):
"""Add CORS headers to responses"""
origin = request.headers.get('Origin')
# Allow requests from specific origins
allowed_origins = ['http://localhost:3000', 'https://myapp.com']
if origin and origin in allowed_origins:
response.headers.add('Access-Control-Allow-Origin', origin)
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
return response
@app.route('/api/data')
def get_data():
return {"message": "This API supports CORS!"}
Using Flask Extensions as Middleware
Many Flask extensions act as middleware. For example:
Flask-Compress for Compression
from flask import Flask
from flask_compress import Compress
app = Flask(__name__)
Compress(app) # Automatically compresses responses
@app.route('/')
def home():
# Response will be automatically compressed
return "This response will be compressed!"
Flask-Caching for Response Caching
from flask import Flask
from flask_caching import Cache
app = Flask(__name__)
cache = Cache(app, config={'CACHE_TYPE': 'SimpleCache'})
@app.route('/expensive-operation')
@cache.cached(timeout=60) # Cache for 60 seconds
def expensive_operation():
# Perform expensive calculation
import time
time.sleep(2) # Simulate work
return "This response is cached for 60 seconds!"
Creating a Custom Middleware Class
For more complex middleware that might be reused across projects, creating a dedicated class is often the best approach:
class RateLimitingMiddleware:
def __init__(self, app, limit=100, window=60):
self.app = app
self.limit = limit # requests
self.window = window # seconds
self.requests = {} # Store request history
def __call__(self, environ, start_response):
# Extract client IP from the request
client_ip = environ.get('REMOTE_ADDR', 'unknown')
path = environ.get('PATH_INFO', '')
# Get current time
import time
current_time = time.time()
# Initialize or clean old records for this IP
if client_ip not in self.requests:
self.requests[client_ip] = []
self.requests[client_ip] = [t for t in self.requests[client_ip]
if current_time - t < self.window]
# Check if rate limit exceeded
if len(self.requests[client_ip]) >= self.limit:
# Rate limit exceeded, return 429 Too Many Requests
status = '429 Too Many Requests'
response_headers = [('Content-Type', 'text/plain')]
start_response(status, response_headers)
return [b'Rate limit exceeded. Please try again later.']
# Add the current request timestamp
self.requests[client_ip].append(current_time)
# Continue with normal request handling
return self.app(environ, start_response)
# Usage
app = Flask(__name__)
app.wsgi_app = RateLimitingMiddleware(app.wsgi_app, limit=5, window=10)
This middleware limits each IP address to 5 requests every 10 seconds, returning a 429 error if the limit is exceeded.
When to Use Different Middleware Approaches
- Use
before_request
andafter_request
hooks for simple middleware that needs access to Flask's request and application context. - Use WSGI middleware when you need to:
- Process the raw WSGI environment
- Modify the response at a lower level
- Intercept and potentially short-circuit requests
- Apply middleware to multiple Flask applications
- Use Flask extensions when you need well-tested implementations of common middleware functionality.
Middleware Order and Execution Flow
The order of your middleware matters! In Flask:
- WSGI middleware executes from outermost to innermost (last added is executed first)
before_request
handlers run in the order they're registered- Your view function executes
after_request
handlers run in the reverse order they're registered- WSGI middleware response processing runs from innermost to outermost
This diagram illustrates the flow:
Request flow: Client → WSGI Middleware → before_request → View → after_request → WSGI Middleware → Client
↓ ↓ ↓ ↓ ↓ ↓
MW1 → MW2 → ... → MWn → BR1 → BR2 → ... → View → AR3 → AR2 → AR1 → MWn → ... → MW1
Summary
Middleware provides a powerful way to add cross-cutting functionality to your Flask applications. In this tutorial, we've explored:
- How middleware works in Flask through request hooks and WSGI middleware
- Building simple middleware using Flask's
before_request
andafter_request
hooks - Creating WSGI middleware for lower-level request and response processing
- Practical examples including authentication, logging, and rate limiting
- Best practices for organizing and implementing middleware
By effectively using middleware, you can keep your route handlers focused on their primary responsibilities while moving cross-cutting concerns into reusable middleware components.
Exercises
To reinforce your learning, try these exercises:
- Create a middleware that adds a custom header with the server processing time.
- Implement a middleware that logs all database queries performed during a request.
- Build a middleware that checks for a maintenance mode flag and serves a maintenance page if enabled.
- Create a caching middleware that stores responses for GET requests for a configurable time period.
- Implement a security middleware that adds recommended security headers to all responses.
Additional Resources
- Flask Documentation on Application Dispatching
- WSGI Middleware Concepts
- Flask Request Hooks Documentation
- Flask Extensions Directory
- Werkzeug Documentation - The WSGI toolkit Flask is built on
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)