Skip to main content

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:

  1. WSGI Middleware - Works at the WSGI application level
  2. Flask's before_request and after_request hooks - Functions that run before/after each request
  3. 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 request
  • after_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:

python
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:

python
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:

python
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:

python
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:

python
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:

python
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

python
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

python
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:

python
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 and after_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:

  1. WSGI middleware executes from outermost to innermost (last added is executed first)
  2. before_request handlers run in the order they're registered
  3. Your view function executes
  4. after_request handlers run in the reverse order they're registered
  5. 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 and after_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:

  1. Create a middleware that adds a custom header with the server processing time.
  2. Implement a middleware that logs all database queries performed during a request.
  3. Build a middleware that checks for a maintenance mode flag and serves a maintenance page if enabled.
  4. Create a caching middleware that stores responses for GET requests for a configurable time period.
  5. Implement a security middleware that adds recommended security headers to all responses.

Additional Resources



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