SSL Mixed Content Warning in Browser

Site is on HTTPS but browser shows a "Not fully secure" warning. Caused by HTTP assets loaded on HTTPS pages.

You finished migrating to HTTPS. The cert is valid, the lock icon appears for two seconds — and then turns into an “i” or a triangle with the message Not fully secure / Your connection to this site is not fully secure. Open DevTools and you’ll see lines like:

Mixed Content: The page at 'https://yourdomain.com/' was loaded over HTTPS,
but requested an insecure element 'http://cdn.example.com/banner.jpg'.
This content should also be served over HTTPS.

Or in stricter cases the resource is silently blocked and your hero image / payment form / map widget never loads at all. This is mixed content, and modern browsers treat it as a security regression — Chrome upgrades passive content automatically since version 86, and blocks active content (scripts, iframes, fetch, XHR) outright. This article walks the diagnosis from “what kind of mixed content do I have?” to a permanent fix.

Passive vs active mixed content (browsers treat them differently)

TypeExamplesChrome behavior
Passive<img>, <audio>, <video> over HTTPAuto-upgrades to HTTPS; falls back to HTTP if upgrade fails; warns in console
Active<script>, <link rel="stylesheet">, <iframe>, fetch(), XMLHttpRequest, WebSocketBlocked outright. The element never loads.

This is why you sometimes see a “Not secure” warning with visible broken behavior (active blocked) and sometimes without (passive only — page looks fine, but the address bar warns).

How to identify which case you’re in

Case 1: Hardcoded http:// in your own templates

Older WordPress themes, hand-written HTML, or templates copy-pasted years ago have absolute http:// URLs in src= / href= / srcset=.

How to spot it:

# Inside your project source
grep -rn 'src="http://' src/ public/ templates/
grep -rn 'href="http://' src/ public/ templates/
grep -rn '"http://' src/ | grep -v 'http://localhost'

In the browser, DevTools → Network → filter “http” → look for non-localhost HTTP requests.

Fix: change http:// to https://. If you don’t know whether the target supports HTTPS, test with curl -I https://that-host.com/path first.

Case 2: Old CMS / database content references HTTP

Your WordPress, Ghost, or self-rolled CMS has thousands of posts with <img src="http://yourdomain.com/wp-content/uploads/..."> hardcoded into post bodies. The template is fine; the database content is not.

How to spot it: DevTools mixed content warnings reference your own domain over HTTP. URLs look like http://yourdomain.com/... not third-party.

Fix (WordPress):

# Use wp-cli, NOT a naive SQL UPDATE — serialized PHP data breaks
wp search-replace 'http://yourdomain.com' 'https://yourdomain.com' \
  --all-tables --skip-columns=guid --dry-run

# Remove --dry-run when satisfied

For other CMSes: run their migration / find-replace tool. Avoid raw SQL on tables containing serialized data (it corrupts length prefixes).

Case 3: Third-party widget / embed only available over HTTP

An analytics script, social-share button, chat widget, or ad slot from a legacy provider that never added HTTPS support. The provider’s HTTPS endpoint either doesn’t exist or returns a cert error.

How to spot it: DevTools shows the mixed-content URL is on a third-party domain. curl -I https://that-provider.com/widget.js either fails or returns SSL errors.

Fix: (a) ask the provider for an HTTPS endpoint — most have one but you’re using a stale snippet; (b) self-host the widget by proxying through your own HTTPS; (c) drop the widget.

Case 4: Protocol-relative URLs (//cdn...) resolving to HTTP in dev

Older code used //cdn.example.com/lib.js (no protocol), which means “match the page’s protocol.” On HTTPS pages it loads via HTTPS; on http://localhost dev it loads via HTTP. Works in prod, breaks in dev, gets misdiagnosed.

How to spot it: warnings appear only on localhost / staging over HTTP, never on prod.

Fix: switch to explicit https://. Protocol-relative URLs were deprecated by the W3C in 2014 because they encourage HTTP fallback paths.

Case 5: Resources loaded by inline JavaScript / fetch / WebSocket

fetch('http://api.example.com/data') or new WebSocket('ws://...') from an HTTPS page. Active mixed content — blocked. The error often appears as a fetch failure in the console, not labeled “mixed content.”

How to spot it: a fetch / XHR rejection in DevTools Network with status (blocked:mixed-content). WebSocket connections to ws:// from HTTPS pages fail with “An insecure WebSocket connection may not be initiated.”

Fix: change http:// to https:// and ws:// to wss:// in your JavaScript. If the API doesn’t support HTTPS, you can’t fix this client-side — you must proxy through your own HTTPS endpoint.

Case 6: <iframe> from HTTP source

Iframes are active content. An <iframe src="http://..."> on an HTTPS page is blocked.

How to spot it: the iframe area is blank or shows a browser error. DevTools console: Mixed Content ... iframe ... was blocked.

Fix: switch to HTTPS source. Some old embed providers (legacy maps, archive.org pre-2017) had HTTP-only embeds — find a newer endpoint or self-host.

Shortest fix path

In hit-rate order:

  1. DevTools → Console → filter “Mixed Content”: get the exact list of offending URLs.
  2. Group them: your domain over HTTP (CMS / DB) vs your templates (find-replace in source) vs third-party (per-provider research).
  3. For your-domain HTTP: run a database search-replace.
  4. For template hardcodes: grep source, replace, commit, redeploy.
  5. For third-party: check if HTTPS endpoint exists; if not, drop or proxy.
  6. As a backstop, add Content-Security-Policy: upgrade-insecure-requests to your HTTP response headers — this tells the browser to auto-rewrite http:// to https:// for all subresources. Covers passive content silently; active content still needs HTTPS to actually work.
# nginx
add_header Content-Security-Policy "upgrade-insecure-requests" always;
<!-- HTML meta as a fallback if you can't set headers -->
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
  1. Verify: hard refresh in an incognito window. DevTools console should be empty. Lock icon should be solid green.

How to verify the fix

# Crawl your own site for HTTP references
# Option A: use Mozilla Observatory
curl -s "https://observatory.mozilla.org/api/v1/analyze?host=yourdomain.com" | jq

# Option B: command-line scan
curl -s https://yourdomain.com/ | grep -oE 'http://[^"]+' | sort -u

# Option C: dedicated scanner (recommended)
# https://www.whynopadlock.com/
# https://www.jitbit.com/sslcheck/
# Both crawl multiple pages and report mixed content per URL

For sites with many pages, a CI job that fails the build if grep -r 'http://' dist/ returns hits is the only reliable long-term safeguard.

Prevention

  • Use protocol-absolute https:// URLs in all new content. Protocol-relative // is deprecated and creates dev/prod inconsistency.
  • Set Content-Security-Policy: upgrade-insecure-requests in your global response headers — it’s a one-line defense against passive mixed content slipping back in.
  • Add a CI check that scans built HTML for http:// outside of comments. Fail the build on a hit.
  • For WordPress / Ghost / CMS sites: configure the canonical site URL once at https:// and validate every theme/plugin update doesn’t regress (some emit hardcoded HTTP).
  • Audit third-party embeds yearly: providers occasionally drop HTTPS or change endpoints. Stale embed snippets are the most common regression source.

FAQ

Q: Will mixed content hurt my Google ranking? A: Not directly, but indirectly yes. HTTPS is a ranking signal since 2014. A page that loads over HTTPS but shows a “Not secure” warning loses user trust and the page’s user-engagement signals (bounce rate, time on page) suffer. Google also blocks mixed-content images in the Chrome address bar, which hurts conversion.

Q: Does upgrade-insecure-requests fix everything? A: It fixes passive content (images, audio, video) silently. It also tries to upgrade active content (scripts, iframes), but if the target server doesn’t actually serve HTTPS, the upgraded request fails and the element doesn’t load. So you still need every target to support HTTPS — upgrade-insecure-requests just removes the need to rewrite every URL manually.

Q: My WordPress site still shows mixed content after I changed the site URL. A: The site URL only controls new content. Existing posts have hardcoded http:// in their content. Run wp search-replace 'http://yourdomain.com' 'https://yourdomain.com' --skip-columns=guid to fix the database. Don’t skip --skip-columns=guid or you’ll break old feed readers.

Q: I see mixed-content warnings only for one specific page. Why? A: That page references something the rest of the site doesn’t — usually a banner image, a custom embed, or a one-off third-party script. Open DevTools on that exact page and check the Network tab for HTTP requests.

Q: A third-party widget I depend on is HTTP-only and the vendor refuses to add HTTPS. What now? A: Proxy it through your own server. Set up a route at https://yourdomain.com/widget-proxy/ that server-side-fetches the HTTP resource and serves it over your HTTPS. Adds latency and bandwidth cost, but it’s the only way short of dropping the widget.

Tags: #Domain #DNS #SSL #Troubleshooting #SSL mixed content