How to Make Django Redirect WWW to Your Bare Domain

If you’re hosting a website on a top level domain, you should set up both the bare domain (example.com) and the “www” subdomain (www.example.com). People expect to be able to type either version and see your site - no matter which version you advertise.
The fashion these days seems to be to use the bare domain - as argued by dropwww.com. That said, some say we still need the “www” - as argued by www.yes-www.org.
Personally, I side with the bare domain crowd. I don’t think any of the technical arguments either way are showstoppers, and it’s nicer to type less.
But anyway, whichever side you’re on, you’ll want to redirect from one to the other. If you don’t, some users won’t find your site.
In this tutorial we’ll set up a www -> bare domain redirect. If you want the opposite, you should be able to follow along and just swap the positions.
Where to Redirect?
You can configure such a redirect at one of several layers in your stack.
DNS Provider
Some DNS providers provide a “redirect” DNS record. This isn’t a real DNS record. Instead, it points the domain at their web servers, which then serve HTTP redirect responses to users.
Unfortunately, most providers that I’ve seen do this still don’t provide HTTPS for the redirect. This means you won’t be able to get top security marks, or use Strict Transport Security.
CDN
If you’re using a CDN, you might be able to configure such a redirect there. For example, CloudFlare provide page rules to do this.
Web Server
If you’re running a “reverse proxy” web server around your application, you can configure the redirect there. Most Django applications use such a web server, and this tends to be where their WWW redirect ends up. It’s appealing since most web servers allow you to do it with a little standard configuration. For example, Nginx provides redirect rules.
But if you’re using a Platform-as-a-Service, such as Heroku or Divio, you might not be able to configure your web server easily—or at all. Additionally if you want to expand to other subdomains it can become hard to test the many rules.
Your Application (Django or Otherwise)
The innermost layer you could do this is in your application. The biggest advantage of this approach is that you can later extend the logic with arbitrary code, such as matching subdomains in our database.
And if your application is in Django, well this is the tutorial for you! But this technique can be extended to other frameworks too. You could even adapt it into a WSGI middleware.
Adding a Middleware
We then want to check inside our Django code for the “www” domain and redirect such requests to the bare domain.
Because we want to run this check on every request, we’ll want to do it in a custom middleware component. Middleware acts like “onion skin” layers around our normal application. It allows us to see every request as it comes in, and every response as it goes out.
Django allows us to write middleware as either a function or a class. I find the class style slightly easier to follow, so let’s use that here.
Add this middleware class to one of your Django applications in a file called middleware.py:
# myapp/middleware.py
from django.http import HttpResponsePermanentRedirect
class WwwRedirectMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
host = request.get_host().partition(":")[0]
if host == "www.example.com":
return HttpResponsePermanentRedirect("https://example.com" + request.path)
else:
return self.get_response(request)
Notes:
- The filename is a convention. Django doesn’t automatically discover middleware, but it’s best for us to store it in a logical location.
- The
__init__method runs when Django starts up. We are passed aget_responsefunction that represents calling the next middleware, or if we’re the last middleware, the view. - The
__call__method is called for each request. We need to return a response from it. This can come either fromget_response, or we can generate it ourself to “shortcut” calling the next middleware/view. - In our
__call__body, we get the requested host with therequest.get_host()method. This will check the standard HTTP “Host” header, but also works if we configure Django to recognize different headers set by intermediate proxies. - We take the full host and split it up using the
str.partitionmethod. This allows us to strip any port clause off the host name, for example turnlocalhost:8000into justlocalhost. - If the host name matches our “www” domain, we return a permanent redirect response, adding the full path to the
example.comdomain. Otherwise, we return the result of theget_responsefunction, allowing the remaining middleware and view to run as normal.
To deploy this, we need to ensure both domains are in our ALLOWED_HOSTS setting:
# settings file
ALLOWED_HOSTS = [
# ...
"example.com",
"www.example.com",
# ...
]
We’ll also need to add our middleware to the MIDDLEWARE setting. It should be as close to the top as possible, but after at least SecurityMiddleware which sets some security headers on all responses:
# settings file
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"myapp.middleware.WwwRedirectMiddleware",
...,
]
Adding Tests
Let’s add some unit tests to check our middleware responds correctly in the main situations we care about.
We might be tempted to test our middleware by making requests to a view using the test client and seeing when we get redirected. These would work, but they wouldn’t be the simplest or fastest. They’d still depend on running all the other middleware and the view.
We can instead write “pure” unit tests by using Django’s RequestFactory. We use this to generate standalone request objects, and see how our middleware responds to them in isolation.
Let’s look at these tests:
# myapp/tests/test_middleware.py
from http import HTTPStatus
from django.test import RequestFactory, SimpleTestCase
from core.middleware import WwwRedirectMiddleware
class WwwRedirectMiddlewareTests(SimpleTestCase):
def setUp(self):
self.request_factory = RequestFactory()
self.dummy_response = object()
self.middleware = WwwRedirectMiddleware(lambda request: self.dummy_response)
def test_www_redirect(self):
request = self.request_factory.get("/some-path/", HTTP_HOST="www.example.com")
response = self.middleware(request)
self.assertEqual(response.status_code, HTTPStatus.MOVED_PERMANENTLY)
self.assertEqual(response["Location"], "https://example.com/some-path/")
def test_www_redirect_different_port(self):
request = self.request_factory.get(
"/some-path/", HTTP_HOST="www.example.com:8080"
)
response = self.middleware(request)
self.assertEqual(response.status_code, HTTPStatus.MOVED_PERMANENTLY)
self.assertEqual(response["Location"], "https://example.com/some-path/")
def test_non_redirect(self):
request = self.request_factory.get("/", HTTP_HOST="example.com")
response = self.middleware(request)
self.assertIs(response, self.dummy_response)
def test_non_redirect_different_port(self):
request = self.request_factory.get("/", HTTP_HOST="example.com:8080")
response = self.middleware(request)
self.assertIs(response, self.dummy_response)
Notes:
We subclass Django’s
SimpleTestCase, rather thanTestCase, because we don’t need the database. (I previously covered how using the right test case classes lead to a 3x speed up for a client’s project.)In
setUp()we construct a few things and store them onselffor use in the test methods. We create aRequestFactoryto allow us to create requests.We create a sentinel
object()to act as a “dummy response.” We use this in the tests to see if the middleware returns the exact value from theget_responsefunction, without needing to construct a full response object. (If you’ve not seen them before, I recommend Trey Hunner’s article on sentinels.)Finally we create an instance of our middleware, passing it a lambda that acts as the
get_responsefunction.Each test method follows the Arrange Act Assert pattern.
We “arrange” by constructing a certain request to pass to our middleware.
We “act” by calling the middleware with that request and storing the returned response.
We then “assert” on that returned response. In the test methods we expect it to redirect, we check for the
HTTPStatus.MOVED_PERMANENTLYstatus code and the location we’d be redirected to. In the others, we check that we got the dummy response object back, indicating the middleware would call the lower layer.
These tests run very fast. This is because they avoid executing other middleware, views, or any database queries:
$ python manage.py test myapp.tests.test_middleware
System check identified no issues (0 silenced).
....
----------------------------------------------------------------------
Ran 4 tests in 0.001s
OK
These unit tests should give us confidence to deploy the change to production. If you make any changes to the middleware, don’t forget to update and rerun the tests.
Deploying
With all the setup and testing, you should be pretty confident that the change will work. Don’t forget that once this is deployed, both domains’ DNS records need to point to your application. You should also make sure your HTTPS certificate includes both domains.
Fin
I hope this helps you set up your domain redirect. Django’s full middleware topic guide is definitely worth a read to learn more about middleware.
—Adam
😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸
One summary email a week, no spam, I pinky promise.
Related posts:
- Django: safely include data for JavaScript in templates
- How to add a robots.txt to your Django site
- How to Score A+ for Security Headers on Your Django Website
Tags: django