Guides / DisallowedHost: Invalid HTTP_HOST header

DisallowedHost: Invalid HTTP_HOST header

You deployed, and every request now fails with DisallowedHost: Invalid HTTP_HOST header. Django answers 400 Bad Request because the request's Host header is not in ALLOWED_HOSTS. It takes one setting to fix; the only question is which host is missing.

The full error looks like this, and the host inside the quotes is the one to look at:

django.core.exceptions.DisallowedHost: Invalid HTTP_HOST header: 'myapp.example.com'.
You may need to add 'myapp.example.com' to ALLOWED_HOSTS.

What it means

Django compares the Host header of every request with ALLOWED_HOSTS. If the host is not in the list, Django refuses the request before your code runs. The check exists because Django uses the host to build absolute URLs, such as the links in password reset emails, and a forged header could point those at an attacker's site.

With DEBUG = True and an empty list, Django quietly allows localhost and 127.0.0.1. That is why this error only appears once you deploy.

Why it happens

Look at the host in the error message. It tells you which of these applies.

  1. The host is your own domain. It is missing from ALLOWED_HOSTS, or you listed example.com and visitors arrive on www.example.com (or the other way around). The default address your host gave you, such as myapp.us-east-1.example-host.com, counts as a separate host too.
  2. The host is an IP address, like 10.0.3.12:8000 or 172.26.4.7. A load balancer health check is calling your container directly by its private IP, and there is no domain in the request. You will see this in your logs every few seconds, and the load balancer may mark your app unhealthy.
  3. The host is an internal name or 127.0.0.1:8000. A reverse proxy such as nginx is forwarding requests without the original Host header, so Django sees the name of the upstream instead.
  4. The host is a stranger's. A random domain or your server's public IP shows up. That is bots and scanners. Django is doing its job.
  5. The entry is malformed. ALLOWED_HOSTS takes bare hostnames. https://example.com, example.com:8000 and a trailing slash never match. An environment variable that is empty gives [''], which matches nothing.

The fix

1. Your domain is missing

Add every hostname people use, one entry each:

# settings.py
ALLOWED_HOSTS = ["example.com", "www.example.com"]

A leading dot matches the domain and all of its subdomains. It is handy when you serve several, as long as you control all of them:

ALLOWED_HOSTS = [".example.com"]  # example.com, www.example.com, app.example.com, ...

Avoid ALLOWED_HOSTS = ["*"]. It switches the protection off. It is only reasonable when something in front of Django already rejects unknown hosts.

2. A health check is calling your container by IP

You can't list every private IP, and Django's ALLOWED_HOSTS has no support for ranges. Most load balancers also can't be told which Host header to send. The clean fix is to answer the health check before Django looks at the host. Django only validates the host the first time something calls request.get_host(), which CommonMiddleware does on every request (and SecurityMiddleware does when SECURE_SSL_REDIRECT is on). A middleware at the very top of the list runs first:

# myproject/middleware.py
from django.http import HttpResponse


class HealthCheckMiddleware:
    """Answer the load balancer before the Host header is validated."""

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if request.path == "/healthz/":
            return HttpResponse("ok")
        return self.get_response(request)
# settings.py
MIDDLEWARE = [
    "myproject.middleware.HealthCheckMiddleware",  # first, on purpose
    "django.middleware.security.SecurityMiddleware",
    # ... the rest of your middleware
]

Then point the load balancer's health check path at /healthz/. Because the middleware answers without touching the database, a failing database will not make the load balancer kill every instance at once. If you want the check to prove more than that Django is up, add the check you need inside __call__.

3. A reverse proxy drops the Host header

Make the proxy pass the original host through. For nginx:

location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
}

If your proxy puts the original host in X-Forwarded-Host instead, set USE_X_FORWARDED_HOST = True. Only do that when the proxy always overwrites that header, because a client can send it too.

4. Bots and scanners

Nothing to fix. If these errors clutter your logs or error tracker, send that one logger to a null handler:

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {"null": {"class": "logging.NullHandler"}},
    "loggers": {
        "django.security.DisallowedHost": {"handlers": ["null"], "propagate": False},
    },
}

Merge this into your existing LOGGING setting if you have one.

Making it stick in production

Don't hardcode hostnames that differ between environments. Read them from an environment variable and drop empty entries:

import os

ALLOWED_HOSTS = [
    host.strip()
    for host in os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")
    if host.strip()
]
export DJANGO_ALLOWED_HOSTS="example.com,www.example.com"

Check it before the next deploy. manage.py check --deploy warns when ALLOWED_HOSTS is empty with DEBUG = False (deployment checklist), and you can test a host directly:

python manage.py check --deploy

# with the server running and DEBUG = False:
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: example.com" http://127.0.0.1:8000/healthz/   # 200
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: evil.test" http://127.0.0.1:8000/             # 400

Once the host is accepted, forms and redirects can still fail on HTTPS. If your next error is a 403 on a form, read CSRF verification failed. Request aborted. If the page loads but has no styling, see Django static files not loading in production.

Questions

Is it safe to set ALLOWED_HOSTS = ['*']?

Only if something in front of Django already rejects unknown Host headers. Django uses the Host header to build absolute URLs, including the links in password reset emails, so accepting any value lets an attacker poison those links. List your real hostnames instead.

Why do I see DisallowedHost errors for domains I have never heard of?

Bots and scanners send requests with random Host headers, or straight to your server's IP address. Django correctly answers 400. You can keep these out of your error reports by logging the django.security.DisallowedHost logger to a null handler, as shown above.

Or skip it

Django Cloud adds your app's address, and any custom domain you connect, to ALLOWED_HOSTS when it builds your app, and it answers the load balancer's health check, which calls the container by its private IP address. Neither needs a settings change from you.

pip install djangocloud-cli
djangocloud deploy

Get startedSee pricing

Free plans are available for students, AWS Activate startups, open-source, non-profit and research projects. See how Django Cloud compares with other ways to host Django.

Django is a registered trademark of the Django Software Foundation. Django Cloud is not affiliated with or endorsed by it. Found a mistake in this guide? Tell us and we will fix it.