# Mixed Content Blocked: HTTPS Page Requesting HTTP

> Fix 'Mixed Content: The page was loaded over HTTPS, but requested an insecure resource': find the http:// URLs, add upgrade-insecure-requests, fix proxies.

Source: https://howhttpworks.com/debug/mixed-content-blocked
Last reviewed: 2026-10-04

Error messages this page covers:
- `Mixed Content: The page at 'https://example.com/' was loaded over HTTPS, but requested an insecure resource 'http://example.com/api/items'. This request has been blocked; the content must be served over HTTPS.`
- `Mixed Content: The page at 'https://example.com/' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint 'http://api.example.com/items'. This request has been blocked; the content must be served over HTTPS.`
- `Mixed Content: The page at 'https://example.com/' was loaded over HTTPS, but attempted to connect to the insecure WebSocket endpoint 'ws://example.com/socket'. This request has been blocked; this endpoint must be available over WSS.`
- `Blocked loading mixed active content “http://example.com/app.js”`

> **TL;DR:** An HTTPS page requested a subresource over `http://`. Browsers block scripts, stylesheets, iframes, fetch/XHR and `ws://` outright and try to upgrade images, audio and video. Find the `http://` URLs in your HTML, JavaScript, CMS data and proxy headers and change them to `https://`; add `Content-Security-Policy: upgrade-insecure-requests` as a safety net.

## What it means

A page loaded over HTTPS is only as secure as the least secure thing it loads. A script fetched over HTTP can be rewritten by anyone on the path, which would give them control of your page, so browsers block "active" mixed content outright. The warning in Chrome's console and the Firefox equivalent:

```text
Chrome:  Mixed Content: The page at 'https://example.com/' was loaded over HTTPS, but requested an insecure resource 'http://example.com/api/items'. This request has been blocked; the content must be served over HTTPS.

Firefox: Blocked loading mixed active content “http://example.com/app.js”
```

Behaviour depends on the kind of request, per the [W3C Mixed Content](https://www.w3.org/TR/mixed-content/) specification as implemented by browsers:

- Blocked: scripts, stylesheets, iframes, `fetch` and `XMLHttpRequest`, WebSockets (`ws://`), web fonts, and anything else that can change the page.
- Auto-upgraded when possible: images, audio and video in current Chrome and Firefox. The browser requests `https://` instead and blocks the resource if that fails. Older browsers or settings may still load these with a warning and no padlock.
- Not mixed content: `http://localhost` and `http://127.0.0.1` are treated as potentially trustworthy, so development setups often mask the problem.

Forms are a separate case. A form on an HTTPS page that posts to an `http://` action is not blocked by mixed content rules, but browsers may show a warning before submit because the data would travel in the clear.

## Who sent it?

The browser, from the page's own markup or JavaScript. The server-side question is who produced the `http://` URL. Check, in this order:

1. The raw HTML: `view-source:` or `curl -s https://example.com/ | grep -oE "(src|href|action)=[\"']http://[^\"']+"`.
2. JavaScript bundles and inline scripts that build URLs (`'http://' + host`, an environment variable such as `API_URL=http://...` baked in at build time).
3. Data your app stores and renders: CMS posts, product descriptions, database columns, user content with absolute `http:` links.
4. Headers and redirects: a `Location: http://...` header, an HTML `<base href>` with HTTP, or an application that generates absolute URLs from `X-Forwarded-Proto` that your proxy is not setting.
5. Third parties: ad tags, widgets and analytics snippets that load further scripts over HTTP.

## Fix it, in order of likelihood

1. Replace hard-coded `http://` URLs with `https://` (or root-relative paths for your own host). Do not use protocol-relative URLs (`//cdn.example.com/x.js`); they were an old workaround and now just add ambiguity.
2. Fix the scheme the application believes it is using when behind a TLS-terminating proxy.
3. Rewrite stored content (WordPress, other CMS databases).
4. Add `upgrade-insecure-requests` while you clean up, then remove third-party URLs that have no HTTPS endpoint.
5. Switch WebSockets to `wss://`.

### Find the offenders

DevTools: open the Console on the failing page; each blocked request is listed with its URL, and the Network tab with the filter `scheme:http` or `mixed-content:displayed` (Chrome) narrows it.

Across the whole site, ask browsers to report instead of waiting for users to hit each page. `upgrade-insecure-requests` is not applied by a report-only policy, so use a restrictive source list to catch plain HTTP loads:

```http
Content-Security-Policy-Report-Only: default-src https: data: blob: 'unsafe-inline' 'unsafe-eval'; report-uri https://example.com/csp-report
```

Reports will list each `blocked-uri` that uses `http:`. Collect them for a week of real traffic, then fix the top offenders first.

Search your source and database for literal URLs:

```bash
# Templates and source
grep -rnE "(src|href|action|url)=?[\"'(]http://" ./src ./templates ./public

# Rendered pages (crawl one URL)
curl -s https://example.com/ | grep -oE "http://[^\"' )<>]+" | sort -u
```

### CSP: upgrade-insecure-requests

```http
Content-Security-Policy: upgrade-insecure-requests
```

The browser rewrites every `http:` subresource URL (and navigations to your own host) to `https:` before sending the request. It does not fall back to HTTP if HTTPS fails. The directive can sit alongside the rest of your policy:

```http
Content-Security-Policy: default-src 'self'; upgrade-insecure-requests
```

The older `block-all-mixed-content` directive is deprecated; browsers already block active mixed content by default.

```nginx
add_header Content-Security-Policy "upgrade-insecure-requests" always;
```

```javascript
// Express (helmet). Helmet's default policy already includes upgrade-insecure-requests,
// so app.use(helmet()) is enough; this form adds it to your own directives.
import helmet from 'helmet'
app.use(
  helmet.contentSecurityPolicy({
    useDefaults: true,
    directives: { upgradeInsecureRequests: [] }
  })
)
```

### Proxy and framework: the app thinks it is on HTTP

When a CDN or load balancer terminates TLS and talks plain HTTP to the app, the app builds absolute URLs (redirects, canonical links, asset URLs, OAuth callbacks) with `http://` unless it reads `X-Forwarded-Proto`. Fix it at the proxy and tell the app to trust it:

```nginx
proxy_set_header X-Forwarded-Proto $scheme;   # nginx terminating TLS in front of the app
```

```javascript
app.set('trust proxy', 1) // Express: req.protocol and req.secure now follow X-Forwarded-Proto
```

```python
# Django settings.py
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
```

### WordPress

Update the site and home URLs to `https://`, then rewrite stored content. `--skip-columns=guid` matters: WordPress GUIDs must not change.

```bash
wp option update home 'https://example.com'
wp option update siteurl 'https://example.com'
wp search-replace 'http://example.com' 'https://example.com' --skip-columns=guid --all-tables --dry-run
wp search-replace 'http://example.com' 'https://example.com' --skip-columns=guid --all-tables
```

Theme options, page builders and serialized widget data are covered by `search-replace`, which handles PHP serialized strings; a plain SQL `REPLACE()` does not and can corrupt them.

### WebSockets

```javascript
const scheme = location.protocol === 'https:' ? 'wss' : 'ws'
const socket = new WebSocket(`${scheme}://${location.host}/socket`)
```

Your proxy must also terminate TLS for the WebSocket path and forward the `Upgrade` and `Connection` headers.

### Cloudflare

Cloudflare's "Automatic HTTPS Rewrites" rewrites `http://` links in HTML to `https://` when it knows the target supports HTTPS. It is a useful stopgap but does not touch URLs built by JavaScript, JSON responses or CSS imported from other hosts, so still fix the source.

## Verify

```bash
# Rendered HTML should contain no http:// subresources for your own hosts
curl -s https://example.com/ | grep -ciE "(src|href)=[\"']http://"

# The header is present
curl -sI https://example.com/ | grep -i content-security-policy
```

Reload with DevTools open and the Console filter set to "Errors" and "Warnings"; the mixed content lines should be gone. Test in a private window so cached pages do not hide the result.

## Related

- [Content-Security-Policy](https://howhttpworks.com/headers/content-security-policy) for the full directive list, including `upgrade-insecure-requests`.
- [Strict-Transport-Security](https://howhttpworks.com/headers/strict-transport-security): once everything is HTTPS, HSTS keeps visitors from landing on HTTP at all.
- [X-Forwarded-Proto](https://howhttpworks.com/headers/x-forwarded-proto) is the header behind most proxy-induced mixed content.
- [HTTPS and TLS](https://howhttpworks.com/guides/https-and-tls) explains what the padlock does and does not promise.
- [Too many redirects](https://howhttpworks.com/debug/err-too-many-redirects) is the neighbouring failure when you try to force HTTPS and the proxy is misconfigured.
