How HTTP Works

Debug guide · you're seeing

  • 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”

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.

Reviewed 5 min readbeginner5 sourcesTry itMarkdown
On this page

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:

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 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:

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:

# 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

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:

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.

add_header Content-Security-Policy "upgrade-insecure-requests" always;
// 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:

proxy_set_header X-Forwarded-Proto $scheme;   # nginx terminating TLS in front of the app
app.set('trust proxy', 1) // Express: req.protocol and req.secure now follow X-Forwarded-Proto
# 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.

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

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

# 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.

Frequently asked questions

What is the difference between active and passive mixed content?

Active mixed content can change the page: scripts, stylesheets, iframes, fetch and XMLHttpRequest calls, and WebSockets. Browsers block it outright. Passive (display) content such as images, audio and video cannot alter the page. Chrome and Firefox now try to upgrade passive content to HTTPS automatically and block it if the HTTPS version is not available, rather than showing a lock-less page.

Does upgrade-insecure-requests fix mixed content?

It fixes it when the target host also serves the same resource over HTTPS. The browser rewrites http: URLs for subresources to https: before sending them, with no fallback to HTTP. If the third-party host has no valid HTTPS endpoint the request fails instead of loading, so you must still fix or remove that URL.

Why do I only see mixed content errors in production?

Locally you usually serve over HTTP or from localhost, which browsers treat as potentially trustworthy, so http: subresources are allowed there. In production the page is HTTPS, often terminated by a proxy, and any hard-coded http: URLs, database content or an app that builds absolute URLs from the wrong scheme turn into mixed content.

How do I find every insecure URL on my site?

Use the DevTools console filtered for mixed content on each template, deploy a Content-Security-Policy-Report-Only header that restricts sources to https: and collect the reports, and search your HTML, CMS database and JavaScript bundles for http:// URLs. A crawler that renders pages catches the ones loaded dynamically.

Can HSTS fix mixed content?

No. Strict-Transport-Security makes the browser use HTTPS for navigations to your own host, but it does not rewrite a page that requests http: resources from other hosts, and it only covers hosts that sent the header. upgrade-insecure-requests is the directive that rewrites subresource URLs.

Sources

  1. MDN: Mixed contentdeveloper.mozilla.org
  2. W3C: Mixed Contentw3.org
  3. MDN: Content-Security-Policy upgrade-insecure-requestsdeveloper.mozilla.org
  4. W3C: Upgrade Insecure Requestsw3.org
  5. RFC 6797: HTTP Strict Transport Securityrfc-editor.org

Keep going

Browse /search