The Simplest WSGI Middleware

Horse and WSGI Barrels

My library apig-wsgi bridges between AWS API Gateway’s JSON format for HTTP requests and Python WSGI applications. Recently Théophile Chevalier opened an issue requesting the library add an extra WSGI environ variable. I closed it by pointing out that it’s not much code to add a custom WSGI middleware to do so (plus the exact key is a bit out of scope for the library).

I guess most Python web developers don’t touch WSGI day to day, so I figured I’d write this short post to share the knowledge.

A Quick Shot of WSGI

The WSGI specification defines an application as a callable takes two positional parameters. These parameters are, with their conventional names:

The application is called for each request, should call start_response and then return an iterable representing the contents of the HTTP response body. There’s a lot of specification around this, but to implement the simplest middleware we don’t need to dive into that.

We want to only change environ and pass everything on to the proxied application. If we have original_app pointing to our base WSGI application, we can make our wrapped application as a function like so:

def wrapped_app(environ, start_response):
    environ["HTTP_FOO"] = "Bar"
    return original_app(environ, start_response)

Keys in a WSGI environ with the pattern HTTP_* represent the request’s HTTP headers, so this middleware adds the header “foo” with value “bar”. It could do something more useful, for example in the issue I linked, Théophile wanted to set the SCRIPT_NAME key, which represents the start of the URL’s path.

To use wrapped_app we would just need to update our host server configuration (say gunicorn or apig-wsgi) to use wrapped_app rather than original_app. Of course, you could name these something else.

In a Full Application

To test this, I added it to a single file Django application based on the template I shared previously. I ran it with Python 3.7 and Django 2.2. Here’s the code:

import os
import sys

from django.conf import settings
from django.core.wsgi import get_wsgi_application
from django.http import HttpResponse
from django.urls import path
from django.utils.crypto import get_random_string

settings.configure(
    DEBUG=(os.environ.get("DEBUG", "") == "1"),
    ALLOWED_HOSTS=["*"],  # Disable host header validation
    ROOT_URLCONF=__name__,  # Make this module the urlconf
    SECRET_KEY=get_random_string(
        50
    ),  # We aren't using any security features but Django requires this setting
    WSGI_APPLICATION=__name__ + ".app",
)


def index(request):
    return HttpResponse(request.headers.get("foo"))


urlpatterns = [
    path("", index),
]

django_app = get_wsgi_application()


def app(environ, start_response):
    environ["HTTP_FOO"] = "Bar"
    return django_app(environ, start_response)


if __name__ == "__main__":
    from django.core.management import execute_from_command_line

    execute_from_command_line(sys.argv)

The implementation uses a few things:

Run DEBUG=1 python app.py runserver and visit the server, and you will see the magical output Bar. Yay!

Going Further

WSGI middleware gets more complicated if you want to do anything to modify the response. It’s easier to create it as a class then.

For more information check out Graham Dumpleton’s post that shows an example full middleware implementation. There’s also a lot of community information linked on the WSGI official site’s learning section.

Fin

Enjoy,

—Adam


Newly updated: my book Boost Your Django DX now covers Django 5.0 and Python 3.12.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: ,