Fix Django “Invalid HTTP_HOST header” (DisallowedHost)
Content updated October 25, 2026.
Why does Django say “Invalid HTTP_HOST header” when I open my app through a tunnel?
Django rejects unknown hosts with DisallowedHost. Add .qurl.site or your tunnel hostname to ALLOWED_HOSTS, keeping localhost. For qURL™ form posts, add the exact browser https origin to CSRF_TRUSTED_ORIGINS; a wildcard trusts other publishers too. With qURL, your app stays on your machine with no public URL, and each link opens once and expires on its own. If you later need one permanent public hostname, ngrok’s dev domain fits.
What you need
- A Django app running with manage.py runserver, opened through a tunnel or qURL link
- Your settings.py, or the settings module you use for development
Steps
Read the host from the error
With DEBUG on, the page is titled “DisallowedHost at /” and says: “Invalid HTTP_HOST header: 'c-k3j2ab.qurl.site'. You may need to add 'c-k3j2ab.qurl.site' to ALLOWED_HOSTS.” The same line is in the runserver log, followed by the 400 response. The host it names is the one to allow.
Add the tool’s domain to ALLOWED_HOSTS, and keep localhost
A leading dot allows the domain and every subdomain under it, at any depth. Keep the first three entries: with DEBUG on and ALLOWED_HOSTS empty, Django allows .localhost, 127.0.0.1 and [::1], but once the list has anything in it that default no longer applies, and localhost itself gets a 400.
# settings.py (development) ALLOWED_HOSTS = ['localhost', '127.0.0.1', '[::1]', '.qurl.site'] # ngrok: 'your-domain.ngrok-free.app' Cloudflare Quick Tunnel: '.trycloudflare.com'Share it through qURL
Publish the running dev server, make a link, and send it to the recipient. They open it and keep that page open while you configure their exact browser origin in the next step. The first publish creates this device’s identity and needs no account. Each link opens once and expires within 24 hours.
qurl publish http://127.0.0.1:8000 qurl share <CRID>Trust the tool’s https origin for form posts
Since Django 4.0, CSRF protection checks the browser’s Origin header. Through any tunnel the browser’s origin is https while runserver sees plain http, so every form post fails with 403 “Origin checking failed”. Ask the recipient to send you the exact https origin from the address bar of the page they just opened (scheme and hostname, without a path). They must keep that page open; do not open their single-use link yourself. Add that origin to CSRF_TRUSTED_ORIGINS, reload the development settings, and ask them to reload the same page. Replace the example r_your-link hostname below; add each new browser origin explicitly. Do not trust https://*.qurl.site: it includes apps run by other publishers. Keep CSRF tokens enabled and leave cookie domains host-only.
# settings.py (development) CSRF_TRUSTED_ORIGINS = ['https://r_your-link.qurl.site'] # ngrok: 'https://your-domain.ngrok-free.app' Cloudflare: 'https://your-tunnel.trycloudflare.com'
What the recipient sees
With both settings in place, pages load through the link and forms post normally.
Which tool should carry your Django dev server?
| Option | Choose it when | Trade-off |
|---|---|---|
| qURL | qURL fits when particular people should try the Django app: allow .qurl.site as a host and explicitly trust each link’s browser origin, with no account. | qURL needs CSRF_TRUSTED_ORIGINS even with SECURE_PROXY_SSL_HEADER set, because a qURL link’s browser origin differs from the host Django receives. |
| ngrok | ngrok fits when you want one stable URL: list your ngrok dev domain in ALLOWED_HOSTS and its https origin in CSRF_TRUSTED_ORIGINS once. | ngrok needs an account and authtoken, the URL is public, and free-plan browser visitors see a warning page first. |
| Cloudflare Quick Tunnels | A Cloudflare Quick Tunnel fits a no-account look: .trycloudflare.com covers its host; add each assigned https origin explicitly when the hostname changes. | A Cloudflare Quick Tunnel URL is public unless you set --allowed-mail, and Quick Tunnels do not support Server-Sent Events. |
| Tailscale Funnel | Tailscale Funnel fits when the machine is already on a tailnet: its ts.net name is stable, so list that exact name and origin. | Tailscale Funnel makes the Django server public to anyone on the internet, with no access control on Funnel traffic. |
Troubleshooting
| If you see | Do this |
|---|---|
| localhost now returns 400 Bad Request too | ALLOWED_HOSTS has entries, so the DEBUG default no longer applies. Add localhost, 127.0.0.1 and [::1] next to the tunnel’s domain. |
| 403 Forbidden: “Origin checking failed - https://r_….qurl.site does not match any trusted origins.” | Add the exact https origin from this browser to CSRF_TRUSTED_ORIGINS, then reload the settings. Do not add a wildcard for all qurl.site origins. The browser’s origin under qURL (r_….qurl.site) differs from the host Django receives (c-….qurl.site), so changing the forwarded scheme alone does not make them match. |
| SECURE_PROXY_SSL_HEADER fixed form posts through ngrok, but not through qURL | Through ngrok or Cloudflare the browser’s origin is https on the same hostname Django sees, so marking requests secure makes them match. Through qURL the origin’s hostname is different and the forwarded scheme is http, so only CSRF_TRUSTED_ORIGINS works. Set SECURE_PROXY_SSL_HEADER only behind a proxy that sets that header itself. |
| qurl publish says the local app is not accepting TCP connections | Start the app first and check that curl works on the same address. qurl publish connects to localhost as 127.0.0.1, so if your app listens only on IPv6 (on macOS or Linux, lsof -iTCP -sTCP:LISTEN -nP shows [::1]), publish 'http://[::1]:PORT' instead, or start the dev server with --host 127.0.0.1 and publish that address. Neither requires listening on your LAN. Keep the quotes: zsh treats the brackets as a pattern. |
| The recipient sees “Access link invalid” | The link was already used or has expired. Run qurl share again and send the new link. |
| The recipient sees “Resource not found” | The resource was deleted after the link was made. Publish the app again, then run qurl share and send the new link. |
| publish rejects http://127.0.0.1 with “only HTTPS URLs are allowed”, or asks you to log in | You have a CLI older than 3.0. On macOS run brew update, then brew upgrade qurl; elsewhere install the latest release again. After updating, qurl --version should print 3.4.1 or later. |
| publish prints no Access: line, or the Access: line says public and you did not ask for public | With no Access: line, the CLI is older than 3.4.1: on macOS run brew update, then brew upgrade qurl; elsewhere install the latest release again. A resource that an older CLI published is public unless you published it with --private, and anyone who has the CRID of a public resource can open it. To make a public resource private, run qurl delete <CRID> and publish again with qurl 3.4.1 or later. The new resource is private and has a new CRID. |
| publish says this Linux session “can’t reach a systemd user manager” | You are in a container or sandbox without a user service manager. Run qurl publish with --foreground; sharing lasts as long as that process runs. |
| publish says “Quota Exceeded” with “protected resource limit reached”, or “Your qURL account has reached a plan limit” | You can see this without an account: a device with no account can keep up to 3 active resources, with links that last up to 24 hours. Run qurl list --status active, then qurl delete <CRID> for any you no longer need (it asks you to confirm; --yes skips that). qurl stop keeps the resource, so it does not free a slot. |
Frequently Asked Questions
Can I set ALLOWED_HOSTS to ["*"]?
It turns the host check off: Django then accepts any Host header, and your code becomes responsible for validating it. List the domains you need instead, in a development settings file.
Why did Django start checking the Origin header?
Django 4.0 made CSRF protection consult the Origin header when the browser sends one, and made CSRF_TRUSTED_ORIGINS values include the scheme: an older entry like .example.com becomes https://*.example.com.
Which values do I add for qURL?
.qurl.site in ALLOWED_HOSTS and the exact https origin from the browser in CSRF_TRUSTED_ORIGINS. The browser uses r_….qurl.site while Django receives c-….qurl.site. Replace r_your-link in the example, and add a new exact origin when the browser hostname changes. A wildcard trusts every publisher’s qURL origin.
Who else can reach my app while the link is open?
Opening the link admits the network address it was opened from for the session, which lasts 1 hour by default. Other devices that share that public IP address, such as people on the same office or café network or VPN, can reach the app until the session ends. By default a link admits whoever opens it; access rules set through the API, such as IP allow and deny lists, can restrict that. Keep your app’s own sign-in for anything sensitive.