I was setting up Django using Gunicorn behind an Nginx proxy the other day, and hit this problem which took a while to find an answer for so I figure I’ll post it here.

The nginx config file looked like this:

upstream gunicorn_django {
    server localhost:8000;

location / {
    try_files $uri @proxy;

location @proxy {
    proxy_pass http://gunicorn_django;

… and all Django would do was return 400 Bad Request every time when accessed through the proxy, even though it worked perfectly when accessed directly.


Thanks to the comment in this Stack Overflow question by Rune Kaagaard, I worked out that nginx was rewriting the Host header before passing to the proxied host:

    Host: gunicorn_django

Django is fussy about the contents of the Host header, and requires it to be valid according to RFC 952, as shown in django.http.request:

host_validation_re = re.compile(r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9:]+\])(:\d+)?$")


The easiest way around it is to prevent Nginx from rewriting the Host header:

location @proxy {
    proxy_set_header Host $http_host;
    proxy_pass http://gunicorn_django;

… as a bonus, your Django applications can now know what Host was originally being asked for.

Alternatively, don’t use _ in your upstream name, and this problem won’t occur!


Worrying about underscores in received Host headers seems like overkill to me, and if they wanted to strictly adhere to RFC 952 they’re being overly permissive anyway, as it requires:

The first character must be an alpha character. The last character must not be a minus sign or period. […] Single character names or nicknames are not allowed.

… although the newfangled RFC 1123 is a little more permissive:

One aspect of host name syntax is hereby changed: the restriction on the first character is relaxed to allow either a letter or a digit.

… and anyway, how do you know those are valid IPv4 or IPv6 addresses? But you really don’t want to go down that rabbit hole because it only leads to madness. I think Postel’s Law should apply here.