The Simplest WSGI Middleware

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:
environ
, a dict of variables that describe the HTTP request.start_response
, a function to call to start generation of the HTTP response.
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:
- The
WSGI_APPLICATION
setting is set to tell Django’s development server to useapp
in the current module. Without this, Django would use its default WSGI application. - A single view,
index
, to echo back the contents of the HTTP header “foo”. (Using Django 2.2’s newrequest.headers
!). get_wsgi_application()
to get Django’s default WSGI application, stored indjango_app
.app
is created as our wrapper as in the previous example.
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.
Learn how to make your tests run quickly in my book Speed Up Your Django Tests.
One summary email a week, no spam, I pinky promise.
Related posts: