---
title: "Mixed Content Errors in Django: The X-Forwarded-Proto Header You're Ignoring"
description: "Are browser consoles blocking your Django static files? Learn how configuring the X-Forwarded-Proto header solves Mixed Content errors instantly."
slug: "mixed-content-errors-in-django-the-x-forwarded-proto-header-you-re-ignoring"
url: "https://furkanbaytekin.dev/blogs/mixed-content-errors-in-django-the-x-forwarded-proto-header-you-re-ignoring"
author: "Furkan Baytekin"
category: "Software"
tags: []
published: 2026-05-23T19:50:34Z
updated: 2026-05-23T19:50:34Z
readTime: 4
cover: "https://myblogcms.furkanbaytekin.dev/uploads/cover/fc0eb536-750e-4c29-ba4c-d7111742e390"
---

You’ve set up your SSL certificate, configured Nginx or Caddy to listen on port 443, and changed your Django settings to use `SECURE_SSL_REDIRECT = True`. You open your browser, hit your domain, and see the glorious lock icon.

Then you open the browser console and see a wall of red:

> **Mixed Content:** The page at `https://yourdomain.com/` was loaded over HTTPS, but requested an insecure stylesheet `http://yourdomain.com/static/css/main.css`. This request has been blocked; the content must be served over HTTPS.

Your images are broken, your styles are missing, and your JavaScript forms are refusing to submit. You double-check your code, and every internal URL uses relative paths or explicitly points to `https`. Why is Django still generating `http://` links?

The culprit isn’t your Django code. It’s a communication breakdown between your reverse proxy and your WSGI/ASGI server (Gunicorn, Uvicorn), and it comes down to a single HTTP header you’re likely ignoring: `X-Forwarded-Proto`.

---

## The Root Cause: The Proxy Blindspot

In a standard production environment, your architecture looks something like this:

```schema
[Browser] --- (HTTPS) ---> [Reverse Proxy (Nginx/Caddy)] --- (HTTP) ---> [Gunicorn/Django]
```

1. The user talks to **Nginx** over a secure, encrypted **HTTPS** connection.
2. Nginx terminates the SSL encryption and passes the raw request downstream to **Gunicorn/Django** over a local **HTTP** connection.

Because the connection between Nginx and Gunicorn is just plain HTTP, **Django believes the user accessed the site via HTTP.**

When Django functions like `request.is_secure()`, `absolute_uri()`, or the `{% static %}` template tag execute, they check the underlying connection protocol. Seeing plain HTTP, Django builds absolute URLs using the `http://` schema. Your browser, rightly protective of security boundaries, blocks these insecure assets, triggering the dreaded **Mixed Content** error.

---

## The Fix, Step 1: Tell Your Proxy to Speak Up

To fix this, your reverse proxy needs to explicitly tell Django what the *original* protocol was before SSL termination happened. We do this using the `X-Forwarded-Proto` header.

**If you use Nginx:**

Add `proxy_set_header X-Forwarded-Proto $scheme;` inside your `location` block.

```nginx
server {
    listen 443 ssl;
    server_name yourdomain.com;

    # ... SSL configurations ...

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        
        # The critical line:
        proxy_set_header X-Forwarded-Proto $scheme; 
    }
}
```

**If you use Caddy:**

Caddy actually handles this proxy header automatically when using the `reverse_proxy` directive, but ensuring your block looks clean is always good practice:

```caddy
yourdomain.com {
    reverse_proxy 127.0.0.1:8000
}
```

---

## The Fix, Step 2: Teach Django to Listen

Passing the header from Nginx isn't enough. By default, Django ignores `X-Forwarded-Proto` for security reasons. If it trusted this header blindly out of the box, a malicious user could spoof it over public HTTP to trick Django into thinking a connection was secure.

You must explicitly tell Django to trust this header *only* when it comes from your trusted proxy. Open your `settings.py` file and add the following configuration:

```python
# settings.py

# Trust the X-Forwarded-Proto header passed by your proxy
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
```

> **Note on Syntax:** Django automatically converts incoming HTTP headers to WSGI environment variables. It upper-cases the name, replaces dashes with underscores, and prepends `HTTP_`. That is why `X-Forwarded-Proto` becomes `HTTP_X_FORWARDED_PROTO` in your configuration tuple.

**What this line does:**

It tells Django: *"If an incoming request contains a header named `X-Forwarded-Proto` with the value `https`, treat `request.is_secure()` as True."*

---

## Step 3: Verify the Solution

Once you have updated your proxy configuration and your `settings.py`, restart both services:

```bash
sudo systemctl restart nginx
sudo systemctl restart gunicorn # or your specific process manager

```

Clear your browser cache (or open an Incognito window) and reload your application. Inspect the console. The Mixed Content errors should be completely gone, and all your dynamic asset links will correctly output `https://`.

---

## Security Warning: Protect Your Headers

Never set `SECURE_PROXY_SSL_HEADER` without ensuring that your reverse proxy is the **only** entry point to your application server.

If your Gunicorn/Uvicorn port (e.g., `8000`) is exposed directly to the public internet, an attacker could send a raw HTTP request with `X-Forwarded-Proto: https` manually attached. Django would mistake it for a secure connection, rendering your security middleware useless against things like session hijacking or insecure cookie transmission.

Ensure your firewall rules blocks public access to your application server ports, leaving only your proxy ports (`80` and `443`) open to the world.
