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.
On this page
TL;DR: An HTTPS page requested a subresource over
http://. Browsers block scripts, stylesheets, iframes, fetch/XHR andws://outright and try to upgrade images, audio and video. Find thehttp://URLs in your HTML, JavaScript, CMS data and proxy headers and change them tohttps://; addContent-Security-Policy: upgrade-insecure-requestsas 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,
fetchandXMLHttpRequest, 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://localhostandhttp://127.0.0.1are 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:
- The raw HTML:
view-source:orcurl -s https://example.com/ | grep -oE "(src|href|action)=[\"']http://[^\"']+". - JavaScript bundles and inline scripts that build URLs (
'http://' + host, an environment variable such asAPI_URL=http://...baked in at build time). - Data your app stores and renders: CMS posts, product descriptions, database columns, user content with absolute
http:links. - Headers and redirects: a
Location: http://...header, an HTML<base href>with HTTP, or an application that generates absolute URLs fromX-Forwarded-Protothat your proxy is not setting. - Third parties: ad tags, widgets and analytics snippets that load further scripts over HTTP.
Fix it, in order of likelihood
- Replace hard-coded
http://URLs withhttps://(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. - Fix the scheme the application believes it is using when behind a TLS-terminating proxy.
- Rewrite stored content (WordPress, other CMS databases).
- Add
upgrade-insecure-requestswhile you clean up, then remove third-party URLs that have no HTTPS endpoint. - 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.
Related
- Content-Security-Policy for the full directive list, including
upgrade-insecure-requests. - Strict-Transport-Security: once everything is HTTPS, HSTS keeps visitors from landing on HTTP at all.
- X-Forwarded-Proto is the header behind most proxy-induced mixed content.
- HTTPS and TLS explains what the padlock does and does not promise.
- Too many redirects is the neighbouring failure when you try to force HTTPS and the proxy is misconfigured.
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
Related
HTTPS Explained: How TLS Secures HTTP
HTTPS is HTTP over TLS. What the TLS handshake does, how certificates prove identity, what HTTPS hides and what it does not, and how to migrate a site safely.
Content-Security-Policy Header: Directives, Nonces and Console Errors
Content-Security-Policy (CSP) limits what a page may load or run. Directives, nonces, strict-dynamic, Report-Only rollout, console errors, helmet and Next.js.
Strict-Transport-Security (HSTS) Header: Setup and Preload
Strict-Transport-Security (HSTS) forces HTTPS for a set time. max-age, includeSubDomains, preload rules, nginx and Apache config, and how to roll it back.
X-Forwarded-Proto
Learn how the X-Forwarded-Proto header identifies the original protocol (HTTP/HTTPS) used by clients connecting through proxies or load balancers.