GuidesMIXED CONTENT

Mixed content blocked when calling an HTTP API

An HTTPS page that calls fetch('http://...') is blocked as mixed content before the request is sent: on 28 October 2026 Chromium 153, Firefox 155 and WebKit 26.6 all rejected it with a TypeError, while http:// images were upgraded to https://. upgrade-insecure-requests rewrites the URL the same way, so it only helps when the API also serves HTTPS, which ip-api.com's free endpoint and Open Notify do not. The free cors.dev proxy fetches http:// targets and returns them over HTTPS with CORS headers, with no signup and about 600 requests a minute per IP.

What an HTTPS page blocks and what it upgrades

A page served over HTTPS that calls fetch() or XMLHttpRequest with an http:// URL makes a mixed active content request, and browsers block it before it is sent, even when the API answers with Access-Control-Allow-Origin: *. That is why code that works on http://localhost breaks once it is deployed to GitHub Pages, Netlify or Vercel.

The test on 28 October 2026 used Chromium 153, Firefox 155 and WebKit 26.6 in Playwright 1.63 (WebKit is Playwright's Linux build). The page was served as https://mixed.example, a secure context in all 3, and called Open Notify's ISS position API, http://api.open-notify.org/iss-now.json, which answers 200 with Access-Control-Allow-Origin: * over HTTP but refuses HTTPS connections.

Requests from an HTTPS page to http:// URLs in Chromium 153, Firefox 155 and WebKit 26.6, checked October 2026
RequestResultConsole
fetch to an http APIBlocked before sending, TypeError in all 3Mixed content error
img with an http srcUpgraded to https and loaded in all 3Upgrade warning in Chromium and Firefox
fetch to http://127.0.0.1Allowed in Firefox, blocked in Chromium and WebKitChromium: Local Network Access; WebKit: mixed content
fetch with upgrade-insecure-requestsSent to https, then 403 or a refused connectionUpgrade notice in Firefox
fetch through proxy.cors.dev200 JSON in all 3Nothing

Images are passive content, so browsers rewrite their URL to https:// and log a warning instead of blocking; Chromium's warning says the request "was automatically upgraded to HTTPS". Loopback is the odd case: Firefox allowed http://127.0.0.1:8002/ok, WebKit blocked it as mixed content, and Chromium refused it under Local Network Access with "Permission was denied for this request to access the loopback address space".

The mixed content messages in each browser

JavaScript only sees a TypeError; the reason is in the Console, and Chrome's Network panel shows the request as (blocked:mixed-content). For fetch('http://api.open-notify.org/iss-now.json') from https://mixed.example/plain:

  • Chromium 153 rejected with TypeError: Failed to fetch and logged Mixed Content: The page at 'https://mixed.example/plain' was loaded over HTTPS, but requested an insecure resource 'http://api.open-notify.org/iss-now.json'. This request has been blocked; the content must be served over HTTPS.
  • Firefox 155 rejected with TypeError: NetworkError when attempting to fetch resource. and logged Blocked loading mixed active content "http://api.open-notify.org/iss-now.json" (Firefox prints curly quotes around the URL).
  • WebKit 26.6 rejected with TypeError: Load failed and logged [blocked] The page at https://mixed.example/plain requested insecure content from http://api.open-notify.org/iss-now.json. This content was blocked and must be served over HTTPS. followed by Not allowed to request resource.

The same TypeError texts appear for CORS failures and network errors, so the console line is what tells them apart; TypeError: Failed to fetch covers the other causes.

Why upgrade-insecure-requests doesn't fix HTTP-only APIs

The usual advice is the Content-Security-Policy directive upgrade-insecure-requests, sent as a header or as <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">. It makes the browser rewrite every http:// URL on the page to https:// before sending. That avoids the block, but only works when the API serves the same path over HTTPS.

With the directive in place, all 3 engines sent http://ip-api.com/json/8.8.8.8 to https://ip-api.com/json/8.8.8.8, which answered 403 with {"status":"fail","message":"SSL unavailable for this endpoint, order a key at https://members.ip-api.com/"}. The Open Notify call went to https://api.open-notify.org/iss-now.json, where the connection was refused and fetch() rejected with TypeError. Firefox logged each rewrite as Content-Security-Policy: Upgrading insecure request followed by the URL.

Call an HTTP-only API through an HTTPS proxy

If you control the API, give it a certificate: that fixes every client at once. If you don't, the request has to leave from somewhere allowed to speak HTTP, such as your own backend or an HTTPS proxy. The free cors.dev proxy accepts any public http:// upstream on ports outside the Fetch standard's blocked list: https://proxy.cors.dev/http://api.open-notify.org/iss-now.json returned 200 JSON with Access-Control-Allow-Origin in all 3 engines from the HTTPS page. The browser only sees an HTTPS request to proxy.cors.dev, so nothing is blocked; the hop from the proxy to the API is still plain HTTP.

Call an HTTP-only API from an HTTPS page
const ISS_NOW = 'http://api.open-notify.org/iss-now.json';

// https pages can't call http:// directly, so they go through an HTTPS proxy
const url = location.protocol === 'https:' ? `https://proxy.cors.dev/${ISS_NOW}` : ISS_NOW;
const response = await fetch(url);

if (!response.ok) {
  const reason = response.headers.get('X-Cors-Error') ?? `HTTP ${response.status}`;
  throw new Error(`ISS position failed: ${reason}`);
}

const { iss_position: position, timestamp } = await response.json();
console.log(position.latitude, position.longitude, new Date(timestamp * 1000).toISOString());

The snippet calls the API directly on http:// pages, such as local development, and through the proxy on https:// pages, and throws with the X-Cors-Error value when the proxy reports a problem. Both branches worked in all 3 engines on 28 October 2026.

The free proxy needs no signup or key, takes GET and HEAD only, allows about 600 requests a minute per IP and caps responses at 6 MiB with a 30-second upstream deadline. It doesn't carry WebSockets, so a ws:// endpoint needs wss:// on the server. cors.dev Pro ($5 a month for 500,000 requests, 7-day trial with 1,000 requests and no card) adds POST and other methods plus custom headers, and allows 3,000 requests a minute per account and 100 in flight.

ip-api.com and other free HTTP endpoints

ip-api.com is the most common case: its docs say "256-bit SSL encryption is not available for this free API". The free endpoint allows 45 requests a minute per IP, reports requests left in X-Rl and seconds until reset in X-Ttl, answers 429 past the limit and is for non-commercial use only; HTTPS needs ip-api.com's Pro plan (13.3 euros a month when paid for 12 months). Through the proxy, two things change:

  • ip-api.com sees the proxy, not your visitor. http://ip-api.com/json/ without an IP returned the proxy's own location (Frankfurt, Germany, ISP Cloudflare, Inc.), so pass the visitor's IP explicitly, as in http://ip-api.com/json/8.8.8.8.
  • The 45-a-minute budget is shared with everyone on the same proxy address. Four calls on 28 October 2026 reported X-Rl values of 44, 39, 44 and 31, and the proxy exposes X-Rl and X-Ttl to JavaScript so your code can back off.

For geolocation over HTTPS without a proxy, see free IP geolocation APIs.

Good questions.

Why does fetch() to an http:// API work on localhost but fail after deploying?

http://localhost is itself an HTTP page, so an http:// API call is not mixed content there. After deploying to an HTTPS host, the same call is mixed active content and the browser blocks it before sending. fetch() then rejects with TypeError: Failed to fetch in Chromium, NetworkError when attempting to fetch resource. in Firefox and Load failed in WebKit.

Can an HTTPS page open a ws:// WebSocket?

No. In Chromium 153, new WebSocket('ws://...') threw SecurityError: Failed to construct 'WebSocket': An insecure WebSocket connection may not be initiated from a page loaded over HTTPS. WebKit 26.6 blocked it as well, and in Firefox 155 the object was created but the connection failed. The server needs a wss:// endpoint.

Why do http:// images still load on my HTTPS site?

Images are passive mixed content, which current browsers upgrade to https:// instead of blocking. In the 28 October 2026 test, all 3 engines requested https:// for an img whose src used http://, and Chromium logged that the request was upgraded automatically. fetch() and XMLHttpRequest are active content and get blocked instead.

Does the proxy encrypt the whole path to an HTTP API?

No. Only the leg between the browser and proxy.cors.dev uses HTTPS; the proxy reaches the http:// API over plain HTTP, so treat the data as public. Anything in the URL, including API keys, is also visible to every visitor in DevTools.

Can an HTTPS page call a server on http://127.0.0.1?

It depends on the browser. From https://mixed.example, Firefox 155 fetched http://127.0.0.1:8002/ok with a 200, WebKit 26.6 blocked it as mixed content, and Chromium 153 refused it under Local Network Access. Serve the local server over HTTPS, or load the page itself from http://localhost during development.