Django DisallowedHost and CSRF 403 after deploy: what Django is checking.
Both errors come from Django comparing the request with your settings. Bad Request (400) means the host isn’t in ALLOWED_HOSTS. A CSRF 403 on a form that posts to its own site usually means Django thinks the request was plain HTTP, because the proxy ended TLS. Fix the host list, trust X-Forwarded-Proto, and add full origins only for cross-origin posts.
Facts checked
The error lines and what each one means
With DEBUG = False the browser only shows a short page. The useful line is in the app’s log. Match it here:
| The log says | What Django checked | Fix |
|---|---|---|
Invalid HTTP_HOST header: 'your-project.canner.app'. You may need to add 'your-project.canner.app' to ALLOWED_HOSTS. (the browser shows Bad Request (400)) † † | The Host header against ALLOWED_HOSTS. With DEBUG = False and an empty list, every request gets a 400 | Add the host. '.canner.app' with a leading dot matches any subdomain; add each custom domain too |
Forbidden (Origin checking failed - https://example.ca does not match any trusted origins.): /accounts/login/ † † † | The browser’s Origin against the scheme and host Django thinks it is serving. If Django believes the request was http, https://example.ca doesn’t match | Same-site form: set SECURE_PROXY_SSL_HEADER. Form or fetch() on another origin: add that origin to CSRF_TRUSTED_ORIGINS |
Referer checking failed - no Referer. † | On HTTPS requests with no Origin, Django requires a Referer from a trusted origin | Don’t send Referrer-Policy: no-referrer on pages with forms; same-origin keeps the check working |
CSRF cookie not set. † | The csrftoken cookie never came back with the POST | Check CSRF_COOKIE_DOMAIN matches the site, and that the page with the form was rendered with {% csrf_token %} |
A system check error at startup about CSRF_TRUSTED_ORIGINS † | Since Django 4.0, entries need a scheme: 'example.ca' and '.example.ca' are no longer valid | Write 'https://example.ca' and 'https://*.example.ca' |
† Competitor details come from each vendor’s own published pages on the dates shown; prices and plans change, so check the vendor before you decide. `Invalid HTTP_HOST header: 'your-project.canner.app'. You may need to add 'your-project.canner.app' to ALLOWED_HOSTS.` (the browser shows `Bad Request (400)`) (2026-10-29) · `Invalid HTTP_HOST header: 'your-project.canner.app'. You may need to add 'your-project.canner.app' to ALLOWED_HOSTS.` (the browser shows `Bad Request (400)`) (2026-10-29) · `Forbidden (Origin checking failed - https://example.ca does not match any trusted origins.): /accounts/login/` (2026-10-29) · `Forbidden (Origin checking failed - https://example.ca does not match any trusted origins.): /accounts/login/` (2026-10-29) · `Forbidden (Origin checking failed - https://example.ca does not match any trusted origins.): /accounts/login/` (2026-10-29) · `Referer checking failed - no Referer.` (2026-10-29) · `CSRF cookie not set.` (2026-10-29) · A system check error at startup about `CSRF_TRUSTED_ORIGINS` (2026-10-29)
What Canner’s proxy sends to Django
On Canner, Caddy terminates HTTPS and forwards each request to your app on 127.0.0.1. It passes the Host header through unchanged, so Django sees the host the visitor typed: your-project.canner.app or your custom domain. It also sets X-Forwarded-Proto: https, X-Forwarded-For and X-Forwarded-Host, and it drops any X-Forwarded-* values a client tries to send itself.
Django is started with gunicorn --bind 127.0.0.1:$PORT <project>.wsgi:application (see the Python docs; list gunicorn in requirements.txt). Gunicorn trusts X-Forwarded-Proto from 127.0.0.1 and ::1 by default and marks those requests as https, so request.is_secure() is already true on a standard deploy. That is why many Django apps on Canner never hit the CSRF error at all.
Set SECURE_PROXY_SSL_HEADER anyway. When Canner can’t find a package holding wsgi.py next to manage.py, it falls back to manage.py runserver, which doesn’t read the header. The setting also keeps your app correct if you ever change the server or its forwarded_allow_ips.
Settings that work on canner.app and a custom domain
- `.canner.app` in `ALLOWED_HOSTS` covers previews. A pull-request preview lives at
branch--project.canner.app, which the leading dot matches. Caddy only routes your own project’s hostnames to your app, so the wildcard doesn’t let another project’s traffic in. - `www` doesn’t need an entry when Serve www is on. Canner answers
www.example.cawith a 301 toexample.cabefore Django sees it (custom domains). - **Never put
https://*.canner.appinCSRF_TRUSTED_ORIGINS.** Other customers’ apps live on that domain, and the setting tells Django to accept their POSTs. List exact origins. For a preview, its own address is inCANNER_URL. - Same-origin forms need no `CSRF_TRUSTED_ORIGINS` at all once Django sees HTTPS. The list is for a frontend on one domain posting to Django on another.
# settings.py
import os
DEBUG = False
# The Host header Django sees is the one the visitor typed.
ALLOWED_HOSTS = [".canner.app", "example.ca"]
# Tell Django the request was HTTPS when the proxy says so.
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Only for forms or fetch() calls that POST from ANOTHER origin.
# Full origins, scheme included: "https://app.example.ca,https://admin.example.ca"
CSRF_TRUSTED_ORIGINS = [
o for o in os.environ.get("CSRF_TRUSTED_ORIGINS", "").split(",") if o
]Why the deploy went green and the site still answers 400
Canner’s health check calls your app on http://localhost:<port>/ (the path is a project setting) and counts any status below 500 as alive. Django answers that probe with a 400 when localhost isn’t allowed, so the deploy succeeds, then every visitor gets Bad Request (400). Don’t add localhost to quiet it; the check is doing its job, and the fix is the real hostnames.
The build log already warns you: [canner] ⚠ django checks the Host header — if every request fails (Django: 400 Bad Request / DisallowedHost; Rails, Sinatra: "Blocked hosts"), allow this app's host and your custom domain:, followed by a suggested ALLOWED_HOSTS line with your app’s <project>.canner.app address.
Check it before and after the fix
Run these from your machine and from the project. check --deploy flags the missing secure-cookie and HTTPS settings in one pass. The CLI streams the app’s log so you can see the exact Invalid HTTP_HOST or Forbidden (…) line as it happens. Set CSRF_TRUSTED_ORIGINS with canner env set or in the dashboard’s environment variables, then redeploy.
# What does Django answer for each host?
curl -sI https://your-project.canner.app/ | head -1
curl -sI https://example.ca/ | head -1
# Django's own pre-flight list for production settings
python manage.py check --deploy
# Watch the app's log while you reproduce the error
canner logs --followThe same errors on other hosts
The cause is the same anywhere a proxy or load balancer ends TLS: Django gets plain HTTP and a hostname it wasn’t told about. What differs is whether the server in front of Django already sets the scheme (gunicorn does, for connections from loopback) and whether the platform rewrites Host. More framework notes are under hosting by framework and on the Django hosting page.
Questions
Why do I get Bad Request (400) only in production?
With DEBUG = True and an empty ALLOWED_HOSTS, Django accepts localhost, 127.0.0.1 and [::1]. With DEBUG = False it accepts only the hosts you list, so a deployed hostname that isn’t in the list gets a 400.
Is ALLOWED_HOSTS = ['*'] safe?
It turns off Django’s host check, so any code that builds absolute URLs from request.get_host(), such as password-reset links, will use whatever Host a request carries. List your real hosts instead.
Do I need CSRF_TRUSTED_ORIGINS for the Django admin?
Not when the admin is on the same host and Django knows the request was HTTPS. The 403 on admin login almost always means SECURE_PROXY_SSL_HEADER is missing or the server doesn’t set the scheme.
Why must CSRF_TRUSTED_ORIGINS include https://?
Since Django 4.0 the setting holds origins, not hostnames, and an origin includes the scheme. 'https://*.example.ca' replaces the old '.example.ca' form.
Django on a host that tells you the hostname.
Canner warns about ALLOWED_HOSTS in the build log and shows the app’s own error lines next to each deploy.