Adam Johnson

Home | Blog | Training | Projects | Colophon

How to Make Django Redirect WWW to Your Bare Domain

2020-03-02

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:

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 SecurityMiddleware which sets some security headers on all responses:

# settings file
MIDDLEWARE = [
    "django.middleware.common.CommonMiddleware",
    "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:

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


Interested in Django or Python training? I'm taking bookings for workshops.


Subscribe via RSS, Twitter, or email:

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

Related posts:

Tags: django