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

ResourceResultReached server asConsole level
<script src>Blockednothingerror
<link rel=stylesheet>Blockednothingerror
<iframe src>Blockednothingerror
fetch()Blocked, promise rejects with TypeError: Failed to fetchnothingerror
XMLHttpRequestBlocked, onerror, status 0nothingerror
new WebSocket('ws://...')Constructor throws SecurityErrornothingerror
<img src>Auto-upgraded, loadedhttps, same port 8991warning
<audio src>Auto-upgraded, loadedhttps, same port 89912 warnings
<video src>Auto-upgraded, loadedhttps, same port 89912 warnings
<form action=http://> submitInterstitial "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:

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

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.