WSGI and Werkzeug under the hood

Flask · Expert · 15 min

Knowing the layer below Flask demystifies servers, middleware and many confusing errors.

WSGI (PEP 3333) is the contract between Python web servers and apps: the app is a callable app(environ, start_response). environ is a dict of CGI-style keys (REQUEST_METHOD, PATH_INFO, wsgi.input, ...). The app calls start_response(status, headers) and returns an iterable of bytes. Gunicorn, uWSGI, Waitress and the dev server all speak this protocol.

A Flask object is exactly such a callable. Flask.__call__ delegates to app.wsgi_app, which:

1. builds a request context from environ and pushes it (and the app context) 2. matches the URL with Werkzeug's routing (app.url_map, a Map of Rules with converters) 3. runs before_request hooks, the view and after_request hooks 4. converts the return value with make_response into a Werkzeug Response 5. calls the response as a WSGI app, then pops the contexts and runs teardowns

Werkzeug supplies the Request and Response classes, routing, HTTP exceptions such as NotFound, the dev server with reloader and debugger, and utilities such as secure_filename. Flask adds the app object, contexts, blueprints, config, Jinja integration and the CLI.

Because wsgi_app is separate from __call__, middleware wraps it: app.wsgi_app = Middleware(app.wsgi_app) keeps app itself as the Flask object, so extensions and app.config still work. Werkzeug's DispatcherMiddleware can mount several apps under different path prefixes.

WSGI is synchronous: one request occupies a worker thread until it returns. That fact underlies Flask's concurrency model and why long-lived connections such as WebSockets need other tools.

Example

from flask import Flask

app = Flask(__name__)

@app.get("/hi")
def hi():
    return "hi from flask"

class Timing:                                   # minimal WSGI middleware
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app
    def __call__(self, environ, start_response):
        def sr(status, headers, exc_info=None):
            return start_response(status, headers + [("X-Wrapped", "yes")], exc_info)
        return self.wsgi_app(environ, sr)

app.wsgi_app = Timing(app.wsgi_app)

def start_response(status, headers, exc_info=None):
    print(status, dict(headers)["X-Wrapped"])
environ = {"REQUEST_METHOD": "GET", "PATH_INFO": "/hi", "SERVER_NAME": "x",
           "SERVER_PORT": "80", "wsgi.url_scheme": "http"}
print(b"".join(app(environ, start_response)))   # called like a WSGI server would
# 200 OK yes
# b'hi from flask'

Remember

  • A WSGI app is app(environ, start_response) returning bytes
  • Flask.__call__ -> wsgi_app: contexts, routing, hooks, view, response
  • Wrap app.wsgi_app (not app) with middleware
  • Werkzeug provides routing, Request/Response and the dev server

Watch out for

  • Replacing app itself with middleware (app = Middleware(app)), which breaks app.config and the CLI.

Check yourself

  1. Why does Flask recommend app.wsgi_app = Middleware(app.wsgi_app) instead of app = Middleware(app)?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: werkzeug.palletsprojects.com/en/stable/