Django DisallowedHost — Invalid HTTP_HOST Header
Fix Django DisallowedHost Invalid HTTP_HOST header by configuring ALLOWED_HOSTS and understanding the proxy forwarded-host header.
Symptoms
- Django returns 400 with "DisallowedHost: Invalid HTTP_HOST header: '...'"
- The site works locally but 400s behind a load balancer
- The error host includes an internal IP or health-check name
Possible causes
- ALLOWED_HOSTS is empty in production, which Django forbids
- The Host header carries the internal hostname rather than the public one
- ALLOWED_HOSTS lacks the domain plus the load balancer health-check host/IP
Diagnosis steps
Reproduce with the offending Host header
curl -i https://app.example.com -H 'Host: internal-lb:8000'Interpretation
The 400 message echoes the exact host that is not allowed, guiding the allowlist entry.
Inspect the effective ALLOWED_HOSTS
python manage.py shell -c "from django.conf import settings; print(settings.ALLOWED_HOSTS)"Interpretation
Confirms which hosts the running process accepts; environment overrides may differ from the file.
Root cause
Django validates the incoming Host header against ALLOWED_HOSTS to prevent host-header attacks. Any request whose host is absent from the list is rejected with DisallowedHost before it reaches a view.
Fix
Set ALLOWED_HOSTS explicitly (the public domain plus health-check host/IP), and configure the proxy to forward the original host via USE_X_FORWARDED_HOST and SECURE_PROXY_SSL_HEADER so the real host is validated.
Risk note
Adding "*" disables host-header protection and enables cache-poisoning and password-reset attacks; never use it in production.
Verify the fix
Request the site through the public domain and confirm 200; request with a bogus Host and confirm it still returns 400, proving the allowlist is intact.
Symptoms
This debugging guide addresses “Django DisallowedHost — Invalid HTTP_HOST Header”. Start from the symptoms, follow the ordered diagnostics, and confirm each hypothesis before moving on; the fix and verification steps then close the loop.
Observed symptoms include:
- Django returns 400 with “DisallowedHost: Invalid HTTP_HOST header: ‘…’”
- The site works locally but 400s behind a load balancer
- The error host includes an internal IP or health-check name
Reproduce and Diagnose
Reproduce the failure in isolation before changing any code. Work through the checks below in order; each one narrows the search space.
Step 1: Reproduce with the offending Host header
curl -i https://app.example.com -H 'Host: internal-lb:8000'
The 400 message echoes the exact host that is not allowed, guiding the allowlist entry.
Step 2: Inspect the effective ALLOWED_HOSTS
python manage.py shell -c "from django.conf import settings; print(settings.ALLOWED_HOSTS)"
Confirms which hosts the running process accepts; environment overrides may differ from the file.
Likely contributors, in order of frequency:
- ALLOWED_HOSTS is empty in production, which Django forbids
- The Host header carries the internal hostname rather than the public one
- ALLOWED_HOSTS lacks the domain plus the load balancer health-check host/IP
Root Cause
Django validates the incoming Host header against ALLOWED_HOSTS to prevent host-header attacks. Any request whose host is absent from the list is rejected with DisallowedHost before it reaches a view.
The Fix
Set ALLOWED_HOSTS explicitly (the public domain plus health-check host/IP), and configure the proxy to forward the original host via USE_X_FORWARDED_HOST and SECURE_PROXY_SSL_HEADER so the real host is validated.
Watch out: Adding “*” disables host-header protection and enables cache-poisoning and password-reset attacks; never use it in production.
# settings.py
ALLOWED_HOSTS = ['app.example.com', '10.0.0.5'] # public domain + LB health-check
# Behind a trusted proxy that sets X-Forwarded-Host
USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
Verify and Prevent
Request the site through the public domain and confirm 200; request with a bogus Host and confirm it still returns 400, proving the allowlist is intact.
Once verified, add a regression test that reproduces the original symptom so the failure cannot silently return.
This class of failure is usually environmental rather than pure logic, so capture the exact command, the environment variables, and the working directory in your bug report. Reproduce it in the smallest possible script, then widen the scope only once the minimal case is understood. A short written checklist for this failure mode saves hours the next time it appears.