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.

, 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 use app in the current module. Without this, Django would use its default WSGI application.

setting is set to tell Django’s development server to use 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 new request.headers !).

, to echo back the contents of the HTTP header “foo”. (Using Django 2.2’s new !). get_wsgi_application() to get Django’s default WSGI application, stored in django_app .

to get Django’s default WSGI application, stored in . 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.

Fin

Enjoy,

—Adam

Working on a Django project? Check out my book Speed Up Your Django Tests which covers loads of best practices so you can write faster, more accurate tests.

Subscribe via RSS, Twitter, or email: Your email address:

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

Related posts:

Tags: django, python

© 2020 All rights reserved.