XMLHttpRequest works over HTTPS the same way it works over HTTP - you just request an https:// URL. What actually blocks requests on a secure page are two browser security rules people confuse with XHR itself: mixed content, where an HTTPS page is forbidden from making an insecure HTTP request, and CORS, where a request to a different origin needs the server’s permission. Neither is a bug in XMLHttpRequest. And for new code, fetch has largely replaced XHR anyway - it does the same job with a cleaner, promise-based API.
Table of contents
- XHR over HTTPS is just a URL
- Mixed content: HTTPS pages cannot call HTTP
- CORS: calling a different origin needs permission
- Why fetch replaced XMLHttpRequest
- Handling errors properly
- How this fits the rest of the stack
- FAQ
XHR over HTTPS is just a URL
There is nothing special to configure. If the page is served over HTTPS and you request an HTTPS URL, XMLHttpRequest works normally:
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data");
xhr.onload = () => console.log(xhr.responseText);
xhr.send();
The https:// in the URL is all it takes - the browser handles the TLS handshake, certificate validation, and encryption transparently. You do not write any encryption code; the security is at the connection layer, below your JavaScript.
So the premise of most https XMLHttpRequest questions is slightly off: HTTPS is not something you do to an XHR, it is a property of the URL you request. What people actually run into are the two browser policies that govern which requests a secure page is allowed to make - mixed content and CORS - which have nothing to do with XHR specifically and everything to do with page security.
Mixed content: HTTPS pages cannot call HTTP
The first thing that actually blocks requests. A page loaded over HTTPS is not allowed to make requests to plain http:// URLs - that is mixed content, and browsers block it:
// on an https:// page:
xhr.open("GET", "http://api.example.com/data"); // BLOCKED - mixed content
xhr.open("GET", "https://api.example.com/data"); // fine
The browser console shows something like Mixed Content: The page was loaded over HTTPS but requested an insecure resource. The reason is sound: an encrypted page making an unencrypted request would leak data over that insecure connection, defeating the point of HTTPS.
The fix is simple - request the HTTPS version of the URL. Every endpoint your secure page calls must itself be HTTPS. If an API you depend on is HTTP-only, that is a real problem to solve on the API’s side, not something to work around in the browser. On a modern site everything should be HTTPS end to end, and mixed content errors are the browser enforcing exactly that.
CORS: calling a different origin needs permission
The second and more common blocker. When your page makes a request to a different origin - a different domain, subdomain, port, or scheme - the browser enforces the Cross-Origin Resource Sharing policy. The request is only allowed if the target server sends headers granting permission:
Access-Control-Allow-Origin: https://yoursite.com
If the server does not send that header, the browser blocks the response and you see the famous No ‘Access-Control-Allow-Origin’ header is present error - even though the request reached the server and it responded. The block is on the browser side, protecting the user.
The critical point: CORS is fixed on the server, not in your JavaScript. No amount of tweaking the XMLHttpRequest object will make a CORS error go away - the server that owns the endpoint has to send the right headers. If it is your own API, add the headers. If it is a third party that does not allow your origin, you route the request through your own backend instead, which is not subject to browser CORS. CORS errors in the console are a server-configuration message, not a client bug.
Why fetch replaced XMLHttpRequest
For new code, fetch is the modern replacement, and it is worth using. The same request in both:
// XMLHttpRequest - callback-based, verbose
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data");
xhr.onload = () => console.log(JSON.parse(xhr.responseText));
xhr.onerror = () => console.error("failed");
xhr.send();
// fetch - promise-based, concise
fetch("https://api.example.com/data")
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));
fetch is promise-based, so it works cleanly with async/await, and its API is far shorter for the common cases. It is built into every modern browser.
XHR is not deprecated and still has a couple of niche advantages - upload progress events, and the ability to be synchronous (which you should almost never use). But for the vast majority of requests, fetch is the clearer choice. The same mixed content and CORS rules apply to fetch exactly as they do to XHR - they are page-security policies, not XHR quirks - so switching to fetch does not make those errors go away. It just gives you a nicer API for the request itself.
Handling errors properly
One behavior difference worth knowing when you move from XHR to fetch: fetch does not treat HTTP error statuses as failures. A 404 or 500 still resolves the promise successfully - only a network failure rejects it:
fetch("https://api.example.com/data")
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`); // check explicitly
return res.json();
})
.then(data => console.log(data))
.catch(err => console.error(err));
You must check res.ok (or res.status) yourself - fetch considers the request a success as long as it got a response, even a 500. This surprises people coming from XHR, where you would inspect xhr.status in onload. Forgetting the res.ok check means treating an error page’s body as if it were valid data.
Whichever you use, handle the failure paths: the network being down, the server returning an error, and the CORS or mixed-content block. Robust client code checks the status, catches the rejection, and degrades gracefully rather than assuming every request returns clean JSON. Over real networks, some requests will fail, and code that ignores that is code that breaks in front of users.
How this fits the rest of the stack
Mixed content and CORS are both really questions about origins and who is allowed to talk to whom - the same boundary thinking that decides how a frontend reaches its backend safely. Serving everything over HTTPS and getting CORS headers right at the API is exactly the kind of thing that should be settled at the platform and service layer, not patched in the browser. The RunxBuild hosting calculator lays out the service, database, storage, and bandwidth as separate line items, and the RunxBuild dashboard is where the team watches deploys, logs, and restarts as they happen.
Useful related references:
- 443 Port TCP: HTTPS, TLS Handshake, and Firewall Rules
- Backend App Development Company: The Five Things the SERP Glosses Over (And What to Ask Instead)
- HTTPS Port: 443, How It Works, and Why It Matters
- Static headers on RunxBuild
FAQ
Does XMLHttpRequest work over HTTPS?
Yes. Request an https:// URL and the browser handles TLS transparently - there is nothing special to configure. HTTPS is a property of the URL, not something you apply to the XHR object. What blocks requests on secure pages is usually mixed content or CORS, not XHR itself.
What is a mixed content error?
It occurs when a page loaded over HTTPS tries to make a request to an insecure http:// URL, which browsers block to prevent leaking data over an unencrypted connection. The fix is to request the HTTPS version of the URL - every endpoint a secure page calls must itself be HTTPS.
How do I fix a CORS error with XMLHttpRequest?
CORS is fixed on the server, not in your JavaScript. The target server must send an Access-Control-Allow-Origin header permitting your origin. If it is your own API, add the header; if it is a third party that does not allow your origin, route the request through your own backend, which is not subject to browser CORS.
Should I use fetch or XMLHttpRequest?
For new code, use fetch. It is promise-based, works with async/await, and has a far cleaner API for common requests. XHR still has niche uses like upload progress events, but fetch is the modern default. The same mixed content and CORS rules apply to both.
Why does fetch not throw on a 404 or 500?
Because fetch only rejects on a network failure, not on HTTP error statuses - a 404 or 500 still resolves the promise. You must check res.ok or res.status yourself and throw if it is not ok, otherwise you risk treating an error page’s body as valid data.