Mixed Content blocked on HTTPS (tested)
A Mixed Content error means an HTTPS page asked for something over plain http://. Chrome blocks scripts, styles, frames, fetch, XHR and WebSocket outright. It silently upgrades images, audio and video to https. Forms get a warning page. We measured every case, the exact console messages, and the fixes.
1. Our test setup
A Node server on port 8990 served an HTTPS page at https://site.test:8990/. A second server on port 8991 answered both plain HTTP and HTTPS on the same port, and logged which one each request used. That shows whether Chrome blocked a request, sent it as http, or upgraded it to https.
The page loaded http://assets.test:8991/... for a script, a stylesheet, an image, an <audio>, a <video>, an iframe, a fetch(), an XMLHttpRequest, a WebSocket('ws://...') and a POST form. Both host names pointed to 127.0.0.1 through a Chromium host-resolver rule, so Chrome treated them as normal remote hosts.
2. Results: what Chromium 153 does with each http:// resource
| Resource | Result | Reached server as | Console level |
|---|---|---|---|
<script src> | Blocked | nothing | error |
<link rel=stylesheet> | Blocked | nothing | error |
<iframe src> | Blocked | nothing | error |
fetch() | Blocked, promise rejects with TypeError: Failed to fetch | nothing | error |
XMLHttpRequest | Blocked, onerror, status 0 | nothing | error |
new WebSocket('ws://...') | Constructor throws SecurityError | nothing | error |
<img src> | Auto-upgraded, loaded | https, same port 8991 | warning |
<audio src> | Auto-upgraded, loaded | https, same port 8991 | 2 warnings |
<video src> | Auto-upgraded, loaded | https, same port 8991 | 2 warnings |
<form action=http://> submit | Interstitial "Form is not secure" | nothing (until the user clicks "Send anyway") | warning |
The upgrade only changes the scheme. Chrome kept the explicit port: http://assets.test:8991/i.png became https://assets.test:8991/i.png. If the host does not speak HTTPS there, the upgraded image just breaks. When we made port 8991 HTTP-only, the image, audio and video all failed with net::ERR_SSL_PROTOCOL_ERROR. There is no fallback to http.
3. The exact console messages
Blocked script (same wording for stylesheet and frame):
Mixed Content: The page at 'https://site.test:8990/' was loaded over HTTPS, but requested an insecure script 'http://assets.test:8991/t1/s.js'. This request has been blocked; the content must be served over HTTPS.
Blocked fetch and XHR:
... but requested an insecure resource 'http://assets.test:8991/t1/fetch'. This request has been blocked; the content must be served over HTTPS.
... but requested an insecure XMLHttpRequest endpoint 'http://assets.test:8991/t1/xhr'. This request has been blocked; the content must be served over HTTPS.
WebSocket (console, plus the thrown exception):
... but attempted to connect to the insecure WebSocket endpoint 'ws://assets.test:8991/t1/ws'. This request has been blocked; this endpoint must be available over WSS.
SecurityError: Failed to construct 'WebSocket': An insecure WebSocket connection may not be initiated from a page loaded over HTTPS.
Auto-upgraded image, audio or video (a warning, not an error):
... but requested an insecure element 'http://assets.test:8991/t1/i.png'. This request was automatically upgraded to HTTPS, For more information see https://blog.chromium.org/2019/10/no-more-mixed-messages-about-https.html
Audio and video also log a second warning: ... requested an insecure audio file '...a.wav'. This content should also be served over HTTPS.
Form:
... was loaded over a secure connection, but contains a form that targets an insecure endpoint 'http://assets.test:8991/t1/form'. This endpoint should be made available over a secure connection.
In Playwright and DevTools Protocol, blocked requests fail with the error text mixed-content. A blocked fetch only gives your code TypeError: Failed to fetch, which looks like every other network failure. See TypeError: Failed to fetch for the other causes.
4. Fix 1: use https:// URLs (best)
Change every http:// to https://. With https://assets.test:8991 everything loaded, all requests arrived over https, and the console was empty. This is the only fix that also works in older browsers and in non-browser clients.
Protocol-relative URLs (//assets.test:8991/s.js) gave the same clean result on an HTTPS page: every request, including the WebSocket built from //assets.test:8991/ws, went over https or wss. They still break when the page itself is opened over http, so prefer plain https://.
5. Fix 2: Content-Security-Policy: upgrade-insecure-requests
If you cannot edit every URL (old CMS content, user posts), send this header on the HTML page:
Content-Security-Policy: upgrade-insecure-requests
With the header, Chromium rewrote all ten requests to https or wss before sending them. The script ran, the CSS applied, the iframe loaded, fetch and XHR returned 200, the WebSocket opened as wss://assets.test:8991/... and the form posted to https://assets.test:8991/form without the warning page. The <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> tag gave exactly the same result.
Three details we saw:
- The port was kept (8991 stayed 8991). Per the CSP spec, only port 80 is changed to 443; we did not measure that.
- The host must really serve HTTPS. With an HTTP-only port 8991, every resource failed with
net::ERR_SSL_PROTOCOL_ERROR, and the console showedWebSocket connection to 'wss://...' failed: Error in connection establishment: net::ERR_SSL_PROTOCOL_ERROR. - For
fetch,response.urlstill reported the originalhttp://URL, although the server log shows the request came over https. XHR'sresponseURLshowedhttps://.
Audio and video still logged the "should also be served over HTTPS" warning under the header, even though they were upgraded. The header is a safety net, not a replacement for fixing the URLs. More detail in upgrade-insecure-requests and Content-Security-Policy header examples.
6. localhost and 127.0.0.1 are allowed
Loopback addresses count as "potentially trustworthy". From the same HTTPS page, http://localhost:8991 and http://127.0.0.1:8991 were not blocked at all. Script, CSS, iframe, fetch, XHR, ws:// and the form POST all went out as plain http with no interstitial. Images, audio and video were not upgraded either, and Chrome logged:
... requested an insecure element 'http://localhost:8991/t6/i.png'. This request was not upgraded to HTTPS because it is a local network request.
... requested an insecure element 'http://127.0.0.1:8991/t7/i.png'. This request was not upgraded to HTTPS because its URL's host is an IP address.
So code that works against a local dev API can fail once the API moves to a real host name. That is when the mixed content errors appear.
7. Find the mixed content on your site
- Open DevTools > Console and filter for
Mixed Content. Each line names the exact URL. - Search your templates and database for
http://insrc=,href=,action=,url(,fetch(andws://. - Behind a TLS-terminating proxy, the app may think it is on http and build http:// links. See X-Forwarded-Proto in nginx.
- Once clean, add Strict-Transport-Security so the page itself is never loaded over http.
Frequently asked questions
Why does Chrome load my http image but block my http script?
In our Chromium 153 test, images, audio and video are auto-upgraded to https and loaded if the host serves HTTPS. Scripts, stylesheets, iframes, fetch, XHR and ws:// WebSockets are blocked with no upgrade.
Does the auto-upgrade change the port?
No. http://assets.test:8991/i.png was requested as https://assets.test:8991/i.png. If that port only speaks HTTP, the image fails with net::ERR_SSL_PROTOCOL_ERROR and there is no fallback.
Does upgrade-insecure-requests fix all mixed content?
It upgraded all ten request types in our test, including fetch, XHR, WebSocket and form posts, as a header or a meta tag. It only works if each host actually serves HTTPS on that port.
Is http://localhost blocked from an HTTPS page?
No. In our test http://localhost and http://127.0.0.1 loaded scripts, fetch, XHR and ws:// without any block. Images were not upgraded, with a 'local network request' or 'host is an IP address' warning.
Why does my fetch just say Failed to fetch?
A mixed-content block rejects fetch with a plain TypeError: Failed to fetch. The real reason is only in the console: 'requested an insecure resource ... This request has been blocked; the content must be served over HTTPS.'
Written with AI assistance. Measured on 2026-10-02 with headless Chromium 153.0.8010.12 (Playwright) and Node.js 24.21.0. An HTTPS page on one port loaded http:// resources from a second port that spoke both HTTP and HTTPS, so we could log which scheme each request really arrived on. Self-signed cert from openssl, ignoreHTTPSErrors on. PlainHeaders is our extension; Chromium, Node.js and openssl are free.