Skip to main content

Flask: Using WSGI Middleware

Flask: Using WSGI Middleware

WSGI (Web Server Gateway Interface) middleware in Flask allows developers to wrap the application to intercept and process requests and responses, enabling functionalities like logging, authentication, or metrics collection. Flask, being a WSGI-compliant framework, seamlessly integrates with middleware to extend its capabilities. This tutorial explores using WSGI middleware in Flask, covering setup, integration with existing middleware, custom middleware creation, and best practices for robust application enhancement.


01. Why Use WSGI Middleware in Flask?

WSGI middleware provides a standardized way to modify request and response flows, centralizing logic for cross-cutting concerns like security, monitoring, or request transformation. Flask’s WSGI-based architecture makes middleware ideal for adding functionality without altering route handlers, ensuring modularity and scalability. Middleware enhances debugging, performance, and security, aligning with modern web development needs.

Example: Basic WSGI Middleware

from flask import Flask
from werkzeug.wrappers import Request, Response

app = Flask(__name__)

class SimpleMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        print(f"Request path: {environ['PATH_INFO']}")
        return self.wsgi_app(environ, start_response)

app.wsgi_app = SimpleMiddleware(app.wsgi_app)

@app.route('/')
def index():
    return "Hello, Flask!"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output:

* Running on http://127.0.0.1:5000
Request path: /

Explanation:

  • SimpleMiddleware - Wraps the Flask WSGI app to log request paths.
  • app.wsgi_app - Integrates the middleware into Flask’s WSGI pipeline.

02. Key WSGI Middleware Techniques

WSGI middleware in Flask can leverage existing libraries, implement custom logic, or integrate with tools like Prometheus or authentication systems. These techniques enhance application functionality and maintainability. The table below summarizes key techniques and their applications:

Technique Description Use Case
Existing Middleware Use libraries like Werkzeug Proxying, authentication
Custom Middleware Implement specific logic Logging, metrics
Chaining Middleware Combine multiple middleware Modular functionality
Response Modification Alter headers/content Add CORS, security headers
Prometheus Integration Expose metrics endpoint Performance monitoring


2.1 Using Existing WSGI Middleware (Werkzeug Proxy)

Example: Proxy Middleware with Werkzeug

from flask import Flask
from werkzeug.middleware.proxy_fix import ProxyFix

app = Flask(__name__)

# Apply ProxyFix for reverse proxy setups
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)

@app.route('/')
def index():
    return f"Client IP: {request.remote_addr}"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output:

* Running on http://127.0.0.1:5000
(Behind proxy: Returns actual client IP instead of proxy IP)

Explanation:

  • ProxyFix - Corrects request metadata (e.g., remote_addr) in reverse proxy setups.
  • Essential for apps behind Nginx or AWS ELB.

2.2 Custom Logging Middleware

Example: Custom Request Logging

from flask import Flask, request
from werkzeug.wrappers import Request, Response
import logging

app = Flask(__name__)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class LoggingMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        req = Request(environ)
        logger.info(f"{req.method} {req.path} from {req.remote_addr}")
        return self.wsgi_app(environ, start_response)

app.wsgi_app = LoggingMiddleware(app.wsgi_app)

@app.route('/')
def index():
    return "Request Logged"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output:

* Running on http://127.0.0.1:5000
INFO:__main__:GET / from 127.0.0.1

Explanation:

  • Logs request details (method, path, IP) for all routes.
  • Uses werkzeug.wrappers.Request for easy request parsing.

2.3 Chaining Multiple Middleware

Example: Combining Middleware

from flask import Flask
from werkzeug.wrappers import Request, Response
from werkzeug.middleware.proxy_fix import ProxyFix
import logging
import time

app = Flask(__name__)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class LoggingMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        req = Request(environ)
        logger.info(f"{req.method} {req.path} from {req.remote_addr}")
        return self.wsgi_app(environ, start_response)

class LatencyMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        start_time = time.time()
        response = self.wsgi_app(environ, start_response)
        latency = time.time() - start_time
        logger.info(f"Latency: {latency:.3f}s")
        return response

# Chain middleware
app.wsgi_app = ProxyFix(LoggingMiddleware(LatencyMiddleware(app.wsgi_app)), x_for=1)

@app.route('/')
def index():
    return "Middleware Chained"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output:

* Running on http://127.0.0.1:5000
INFO:__main__:GET / from 127.0.0.1
INFO:__main__:Latency: 0.002s

Explanation:

  • Chains ProxyFix, LoggingMiddleware, and LatencyMiddleware.
  • Each middleware processes requests in sequence, ensuring modularity.

2.4 Response Modification Middleware

Example: Adding Security Headers

from flask import Flask
from werkzeug.wrappers import Request, Response

app = Flask(__name__)

class SecurityHeadersMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        def custom_start_response(status, headers, *args):
            headers.extend([
                ('Content-Security-Policy', "default-src 'self'"),
                ('X-Content-Type-Options', 'nosniff')
            ])
            return start_response(status, headers, *args)
        return self.wsgi_app(environ, custom_start_response)

app.wsgi_app = SecurityHeadersMiddleware(app.wsgi_app)

@app.route('/')
def index():
    return "Secure Response"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output:

* Running on http://127.0.0.1:5000
(Response headers include: Content-Security-Policy, X-Content-Type-Options)

Explanation:

  • Adds security headers to all responses.
  • Customizes start_response to modify headers.

2.5 Prometheus Metrics Middleware

Example: Prometheus Metrics Integration

from flask import Flask
from prometheus_client import Counter, make_wsgi_app
from werkzeug.middleware.dispatcher import DispatcherMiddleware

app = Flask(__name__)

request_count = Counter('flask_requests_total', 'Total requests', ['method', 'path'])

class MetricsMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        req = Request(environ)
        request_count.labels(method=req.method, path=req.path).inc()
        return self.wsgi_app(environ, start_response)

# Add metrics endpoint and middleware
app.wsgi_app = DispatcherMiddleware(MetricsMiddleware(app.wsgi_app), {
    '/metrics': make_wsgi_app()
})

@app.route('/')
def index():
    return "Metrics Tracked"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output (http://127.0.0.1:5000/metrics):

flask_requests_total{method="GET",path="/"} 1.0

Explanation:

  • Tracks request counts with Prometheus metrics.
  • DispatcherMiddleware exposes metrics at /metrics.

2.6 Incorrect Middleware Setup

Example: Invalid Middleware

from flask import Flask

app = Flask(__name__)

class InvalidMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app
    # Missing __call__ method

app.wsgi_app = InvalidMiddleware(app.wsgi_app)

@app.route('/')
def index():
    return "This will fail"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output:

* Running on http://127.0.0.1:5000
TypeError: 'InvalidMiddleware' object is not callable

Explanation:

  • Missing __call__ method breaks WSGI compatibility.
  • Solution: Implement __call__(self, environ, start_response).

03. Effective Usage

3.1 Recommended Practices

  • Use existing middleware libraries and chain custom middleware for modular design.

Example: Comprehensive WSGI Middleware Setup

from flask import Flask, request
from werkzeug.wrappers import Request, Response
from werkzeug.middleware.proxy_fix import ProxyFix
from prometheus_client import Counter, make_wsgi_app
from werkzeug.middleware.dispatcher import DispatcherMiddleware
import logging
import time

app = Flask(__name__)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

request_count = Counter('flask_requests_total', 'Total requests', ['method', 'path'])

class LoggingMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        req = Request(environ)
        logger.info(f"{req.method} {req.path} from {req.remote_addr}")
        return self.wsgi_app(environ, start_response)

class MetricsMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        req = Request(environ)
        request_count.labels(method=req.method, path=req.path).inc()
        return self.wsgi_app(environ, start_response)

class SecurityHeadersMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        def custom_start_response(status, headers, *args):
            headers.extend([('Content-Security-Policy', "default-src 'self'")])
            return start_response(status, headers, *args)
        return self.wsgi_app(environ, custom_start_response)

# Chain middleware
app.wsgi_app = DispatcherMiddleware(
    ProxyFix(
        LoggingMiddleware(
            MetricsMiddleware(
                SecurityHeadersMiddleware(app.wsgi_app)
            )
        ),
        x_for=1
    ),
    {'/metrics': make_wsgi_app()}
)

@app.route('/')
def index():
    return "Comprehensive Middleware"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output:

* Running on http://127.0.0.1:5000
INFO:__main__:GET / from 127.0.0.1
(Metrics at /metrics: flask_requests_total{method="GET",path="/"} 1.0)
(Response includes Content-Security-Policy header)
  • Combines ProxyFix, logging, metrics, and security headers.
  • Exposes Prometheus metrics and ensures secure, monitored requests.

3.2 Practices to Avoid

  • Avoid blocking operations in middleware to prevent performance issues.

Example: Blocking Middleware

from flask import Flask
from werkzeug.wrappers import Request, Response
import time

app = Flask(__name__)

class BlockingMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        time.sleep(2)  # Simulate blocking operation
        return self.wsgi_app(environ, start_response)

app.wsgi_app = BlockingMiddleware(app.wsgi_app)

@app.route('/')
def index():
    return "Slow Response"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output:

* Running on http://127.0.0.1:5000
(Requests delayed by 2 seconds)
  • Blocking operations in middleware degrade performance.
  • Solution: Use asynchronous tasks or optimize middleware logic.

04. Common Use Cases

4.1 Request Authentication

Enforce authentication for protected routes.

Example: Authentication Middleware

from flask import Flask, jsonify
from werkzeug.wrappers import Request, Response

app = Flask(__name__)

class AuthMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app
        self.valid_key = "secure-key"

    def __call__(self, environ, start_response):
        req = Request(environ)
        if req.path.startswith('/api/'):
            if req.headers.get('X-API-Key') != self.valid_key:
                res = Response('Invalid API Key', status=401)
                return res(environ, start_response)
        return self.wsgi_app(environ, start_response)

app.wsgi_app = AuthMiddleware(app.wsgi_app)

@app.route('/api/secure')
def secure():
    return jsonify({'message': 'Secure data'})

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output:

* Running on http://127.0.0.1:5000
(Request to /api/secure with X-API-Key: secure-key: Returns {"message": "Secure data"})
(Request with invalid key: Returns 401 Invalid API Key)

Explanation:

  • Protects /api/ routes with API key authentication.
  • Centralizes auth logic for all API endpoints.

4.2 Performance Monitoring

Collect performance metrics for all requests.

Example: Performance Metrics Middleware

from flask import Flask
from werkzeug.wrappers import Request, Response
from prometheus_client import Histogram, make_wsgi_app
from werkzeug.middleware.dispatcher import DispatcherMiddleware
import time

app = Flask(__name__)

request_latency = Histogram('flask_request_latency_seconds', 'Request latency', ['path'])

class PerformanceMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        req = Request(environ)
        start_time = time.time()
        response = self.wsgi_app(environ, start_response)
        latency = time.time() - start_time
        request_latency.labels(path=req.path).observe(latency)
        return response

app.wsgi_app = DispatcherMiddleware(PerformanceMiddleware(app.wsgi_app), {
    '/metrics': make_wsgi_app()
})

@app.route('/')
def index():
    return "Performance Monitored"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output (http://127.0.0.1:5000/metrics):

flask_request_latency_seconds_sum{path="/"} 0.002

Explanation:

  • Tracks request latency with Prometheus metrics.
  • Exposes metrics for monitoring and analysis.

Conclusion

Using WSGI middleware in Flask enhances application functionality with minimal code changes. Key takeaways:

  • Leverage existing middleware like ProxyFix for common tasks.
  • Implement custom middleware for logging, authentication, or metrics.
  • Chain middleware for modular, reusable logic.
  • Avoid blocking operations to maintain performance.

With these practices, you can build scalable and maintainable Flask applications with powerful middleware enhancements!

Comments