HTTP Status 0
Status 0 is what a browser reports when a request ends without an HTTP response the page is allowed to read, and no server ever sends it. The request was blocked, cancelled, or failed on the way, or the server answered and the browser withheld the answer. The browser never tells your script which, so the fix starts with finding out.
Updated September 2026 · 6 min read
-
Written by
Andrian Valeanu
Founder of Pulsetic
Andrian Valeanu founded Pulsetic. Before that, Designmodo. The 15-plus years in between went into web products, design tools and monitoring software that teams around the world run on.
-
Reviewed by
Ionut Caval
Technical reviewer
Ionut Caval reads every one of these guides before it goes out. Web servers, networking and uptime monitoring are his day job, so the causes and fixes here get checked against how things actually behave in production.
The short version: Repeat the request with DevTools open and read the console and Network panel. A CORS message means the server must send an Access-Control-Allow-Origin header that matches your page. net::ERR_BLOCKED_BY_CLIENT means an extension blocked it. (canceled) means a navigation or your own code aborted it. If none of those appear, the connection failed: check DNS, the certificate, and that the server is up.
Key takeaways
- Status 0 is not an HTTP status code. It is what the browser reports when a request ended without a response the page is allowed to read, and no server ever sends it.
- The usual causes are a CORS failure, an extension or filter blocking the request, a request cancelled by navigation or by your own code, and network failures such as DNS, TLS or a dropped connection.
fetch()does not return status 0 for these. It rejects with aTypeErrorthat readsFailed to fetch,NetworkError when attempting to fetch resource.orLoad failed, depending on the browser.- The real reason is in DevTools, not in your script. Browsers keep it from page code on purpose and print it to the console and the Network panel instead.
- A server error without CORS headers arrives as status 0, so set those headers at the proxy and on error responses, or every 500 and 502 behind them is disguised.
- Error type
- Browser-reported, not an HTTP status
- Whose side
- Browser or network; CORS is fixed on the server
- Fix difficulty
- Medium
- Common cause
- The request ended without a readable response
How did you find out this time?
Pulsetic is website uptime monitoring. It checks your URL from outside your network as often as every 30 seconds, confirms any failure from another region, then emails you.
10 monitors free, email alerts on failure and recovery. No credit card.
What does HTTP Status 0 mean?
HTTP status codes run from 100 to 599, and every one of them is sent by a server. Status 0 is different, because no server sent it. It is the value the browser gives XMLHttpRequest.status when a request finishes without a response the page is allowed to see. The request may have been blocked before it left, cancelled halfway, refused by the network, or answered by a server whose response the browser then withheld. Your script gets the same zero in every case, and the browser keeps the reason out of reach on purpose: telling a page exactly why a cross-origin request failed would let it learn things about other sites.
The newer fetch() API reports the same failures differently. Instead of resolving with status 0, it rejects its promise with a TypeError whose message depends on the browser: Failed to fetch in Chrome and Edge, NetworkError when attempting to fetch resource. in Firefox, and Load failed in Safari. Pulsetic’s real-user monitoring records both forms as status 0, and across the sites it watches that is where most failed browser requests land, as the uptime statistics report shows. The one time fetch hands back a genuine status 0 is an opaque response, from a no-cors request or a redirect: "manual" request that met a redirect, and that is by design rather than a failure.
- 0
- Reported when no readable response arrived
- 100–599
- The range real HTTP status codes use
- 4
- Usual causes: CORS, blocking, cancelling, the network
How the HTTP Status 0 error appears
The wording changes depending on your browser, device, or server. Here is how this error commonly shows up:
TypeError: Failed to fetch
The request ended before any HTTP response the page could read.
status 0
xhr.status === 0XMLHttpRequest after a network error, an abort or a CORS failureTypeError: Failed to fetchChrome and Edge, whenfetch()rejectsTypeError: NetworkError when attempting to fetch resource.FirefoxTypeError: Load failedSafariAbortErrorfetch()cancelled through anAbortControllernet::ERR_BLOCKED_BY_CLIENTChrome console, when an extension blocked the requestCORS errorStatus column of the Chrome DevTools Network panelstatus: 0, statusText: "error"jQuery$.ajaxand similar wrappers
Status 0 vs a real error status
A status 0 and a real 4xx or 5xx both mean the request failed, but only the real code tells you a server answered.
| What the browser reports | What happened | Where to look |
|---|---|---|
Status 0 or Failed to fetch |
No readable HTTP response: blocked, cancelled, refused by CORS, or the connection failed. | The browser console and Network panel, not the server logs. |
A 4xx such as 403 or 422 |
The server answered and rejected the request. | The response body and the application logs. |
A 5xx such as 500 or 502 |
The server, or a proxy in front of it, answered and failed. | Server, proxy and application logs. |
| An opaque response with status 0 | A no-cors request got a response the page may not read, not even its status. |
Nowhere. It is expected, not a failure. |
What DevTools shows, and what it means
The browser keeps the reason from your script but prints it in DevTools. Match what you see there to the cause.
| What you see | Likely cause | Where the fix goes |
|---|---|---|
Console: blocked by CORS policy | The response had no matching Access-Control-Allow-Origin, or the preflight failed | The server or the proxy in front of it |
net::ERR_BLOCKED_BY_CLIENT | An ad blocker or privacy extension matched the URL | The visitor's browser, or a less tracker-like URL |
Status column: (canceled) | A navigation, an abort() call, or a newer request replaced it | Usually nowhere: filter it out of error reports |
(blocked:mixed-content) | An HTTPS page requested an http:// URL | Serve the endpoint over HTTPS |
net::ERR_CERT_ or ERR_SSL_ codes | The TLS handshake failed | The certificate or TLS setup |
net::ERR_NAME_NOT_RESOLVED, ERR_CONNECTION_REFUSED | DNS or the connection failed before HTTP began | DNS, the firewall, or the server itself |
net::ERR_INTERNET_DISCONNECTED | The visitor went offline | Nowhere server-side; retry when back online |
What causes HTTP Status 0?
- A CORS failure. The server answered, but without an
Access-Control-Allow-Originheader matching the calling page, so the browser discarded the response. A preflightOPTIONSrequest that fails or returns an error status has the same effect. - A server error missing its CORS headers. Many setups add the headers in application code, so a 500 or 502 produced by a proxy, a gateway or a crashed handler goes out without them, and your script sees status 0 instead of the real code.
- An ad blocker, privacy extension or corporate filter stopping the request before it is sent, usually because the URL contains a word like
analytics,trackorads. - The request being cancelled: the visitor navigated away or closed the tab, your code called
abort(), or a timeout you set on anXMLHttpRequestexpired first. - Mixed content: an HTTPS page calling an
http://endpoint, which browsers block outright. - A network failure before any HTTP exchange: a hostname that does not resolve, a refused or reset connection, a certificate the browser rejects, or a visitor who has gone offline.
- A
no-corsor manual-redirect fetch, both of which return status 0 by design rather than because anything failed.
How to find the cause fast
- Reproduce the request with DevTools open. The console and the Network panel show what the script is not told: a CORS message,
ERR_BLOCKED_BY_CLIENT,(canceled), or anet::ERR_code naming the network failure. - Send the same request with curl, including the
Originheader your page sends. A normal response with noAccess-Control-Allow-Originheader in it confirms CORS as the cause. - Try a private window with extensions turned off. If the request succeeds there, an extension was blocking it.
- Check when it happens. Status 0 clustered around page unloads or route changes is cancellation, not failure; status 0 on every request to one host points at that host, its certificate or its CORS setup.
How HTTP Status 0 looks from the outside
Status 0 is invisible from the server. The request either never arrived or arrived and was answered normally, so access logs, APM and a server-side check can all look healthy while visitors fail. The one case that does leave a trace is the one that matters most: a server error missing its CORS headers, where the real 500 or 502 sits in your logs while the browser reports only a zero. Seeing status 0 at all takes measurement from the visitor’s side, which is what real-user monitoring records, and telling a broken endpoint apart from cancelled requests and blocked trackers takes an external check confirming that the endpoint itself still answers.
How to fix HTTP Status 0
If you write the code making the request
- Open DevTools and read the console before changing anything. The browser keeps the reason from your script but prints it there.
- Fix a CORS error on the server, not in the client. Setting
mode: "no-cors"silences the error but gives you an opaque response you cannot read, so it only suits requests whose result you never need. - Handle cancellation separately from failure. Check for
err.name === "AbortError"and ignore it, so a navigation or a superseded request neither shows the visitor an error nor floods your logs. - Call every endpoint over HTTPS from an HTTPS page, and keep API paths free of tracker-like words if an ad blocker turns out to be the cause.
- Retry idempotent requests once the connection returns, and treat
navigator.onLineas a hint rather than a guarantee, since it can report online on a network that goes nowhere.
If you run the server or API
- Send
Access-Control-Allow-Originfor every origin that should call you, and answer theOPTIONSpreflight with a 2xx that includesAccess-Control-Allow-MethodsandAccess-Control-Allow-Headers. - Add the CORS headers at the outermost layer, the proxy or the gateway, so they appear on error responses as well. In nginx that means
add_header ... always, because withoutalwaysthe header is left off 4xx and 5xx responses. - For requests that carry cookies or credentials, return the exact origin rather than
*and addAccess-Control-Allow-Credentials: true, since browsers reject a wildcard on credentialed requests. - Keep a valid certificate and serve the API over HTTPS only, so mixed-content blocking and TLS failures never reach visitors.
- In your error reporting, drop aborted requests and split the remaining status 0 entries by host, so real network and CORS failures are not buried under cancellations.
Still not fixed? Next steps
- Compare browsers and networks. A failure confined to one browser points at its tracking protection or an extension; a failure confined to one network points at a proxy, firewall or DNS filter on that network.
- Look for a security layer answering in your place. A WAF or CDN rule that serves its own block page usually does so without your CORS headers, so the browser reports status 0 instead of the 403 it actually received.
- Inspect the preflight on its own in the Network panel. A preflight that redirects, demands authentication, or returns 401 or 404 fails the whole request even when the real call would have succeeded.
- If the zero shows up in monitoring data and nobody can reproduce it, look at the timing. Clusters at page unload are cancellations; a sudden rise across all visitors after a deploy points at CORS or a certificate change.
Code & configuration
Copy-paste starting points. Replace example.com and the paths with your own, and test changes on staging before production.
Tell a cancelled request from a real failure
try {
const res = await fetch("/api/data", { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`); // a real 4xx or 5xx
return await res.json();
} catch (err) {
if (err.name === "AbortError") return; // cancelled on purpose, not a failure
// a TypeError here means no readable response: CORS, a blocker or the network
reportError(err);
}
CORS headers on every response in nginx, errors included
location /api/ {
# "always" also covers 4xx and 5xx; without it nginx drops the header on errors
add_header Access-Control-Allow-Origin "https://app.example.com" always;
if ($request_method = OPTIONS) {
# add_header here replaces the outer ones, so repeat the origin
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Max-Age 86400 always;
return 204;
}
proxy_pass http://app;
}
Check CORS from the command line
curl -i https://api.example.com/data -H "Origin: https://app.example.com"
# look for: access-control-allow-origin: https://app.example.com
# missing? a browser discards this response and reports status 0
curl -i -X OPTIONS https://api.example.com/data \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST"
# the preflight must return a 2xx with the allow headers
How to prevent HTTP Status 0
Status 0 is the failure a server-side check cannot see, because the request either never arrived or was answered normally and then discarded by the browser. Pulsetic real-user monitoring records failed requests from your actual visitors, status 0 included, alongside their Core Web Vitals. Pair it with uptime checks from multiple locations, which confirm a failure from another region before they alert you, and you can tell a broken endpoint from a burst of cancelled requests or blocked trackers.
Learn how Pulsetic's real-user monitoring detects this from the outside, across 15+ locations.
Sources and further reading
The specifications and vendor documentation this guide is written from, plus deeper reading on the parts it only summarises.
Frequently asked questions
-
What does HTTP status 0 mean?
It means the browser has no HTTP response to give your code. Either the request never completed, because it was blocked, cancelled or failed at the network level, or a response arrived that the browser would not let the page read, which is what happens with CORS. No server sends 0. It is the placeholder the browser reports in place of a real status.
-
Is status code 0 a real HTTP status code?
No. Real status codes run from 100 to 599 and are sent by servers. Zero comes from the browser's own APIs: it is the value of
XMLHttpRequest.statusafter a network error or an abort, and the status of the opaque responses thatno-corsand manual-redirect fetches return. You will not find it among the status codes any HTTP specification defines. -
Why does my request work in curl or Postman but return status 0 in the browser?
Almost always CORS. Curl and Postman are not browsers and do not enforce the same-origin policy, so they show whatever the server sent. A browser checks that response for an
Access-Control-Allow-Originheader matching your page and, if it is missing or wrong, discards the response and reports status 0. The fix is on the server: send the header, and make sure theOPTIONSpreflight succeeds too. -
Can an ad blocker cause status 0?
Yes. Blocking extensions, and the tracking protection built into some browsers, cancel requests whose URLs match their filter lists, and the page sees status 0 or a failed fetch. Chrome prints
net::ERR_BLOCKED_BY_CLIENTin the console when this happens. Paths containing words likeanalytics,trackoradsare the usual victims, even when they serve something else entirely. -
Why does fetch() not return status 0?
Because
fetch()reports these failures as a rejected promise rather than as a response. A CORS failure, a blocked request or a dropped connection makes it reject with aTypeError, and an aborted request rejects with anAbortError. The only responses fetch resolves with a status of 0 are opaque ones, fromno-corsrequests and manual redirects, and those are expected rather than errors. -
Is status 0 always a problem?
No, and in monitoring data much of it is noise. Visitors navigating away mid-request, single-page apps cancelling superseded requests, and extensions blocking trackers all produce it without anything being broken. What deserves attention is a change: status 0 that suddenly rises across all visitors, or that concentrates on one host, usually means a CORS, certificate or availability problem with that host.
-
Why do my 500 errors show up as status 0?
Because the error response is missing its CORS headers. When the headers are added by application code and the failure happens in a proxy, a gateway or a handler that crashed before adding them, the 500 or 502 reaches the browser without
Access-Control-Allow-Originand is discarded like any other CORS failure. In nginx,add_headerleaves error responses out unless you add thealwaysparameter. Put the headers at the outermost layer and the real status comes through. -
How do I tell a CORS error from a network error in JavaScript?
You cannot, and that is deliberate. Browsers make the two look identical to scripts, a status of 0 or a
fetch()rejected with a genericTypeError, so that a page cannot probe other sites and learn about them from the way its requests fail. The difference is visible only in the console and the Network panel, which is why diagnosis starts in DevTools rather than in your error handler. -
What is an opaque response?
It is what
fetch()returns for a request made withmode: "no-cors": a response whosetypeisopaque, with a status of 0 and no readable headers or body. The request did go out and may well have succeeded, but the page is not allowed to see anything about the result. It suits fire-and-forget requests and service worker caching. It is not a way around CORS, because the data you wanted is exactly what it hides. -
Does a timeout return status 0?
For
XMLHttpRequest, yes: when thetimeoutyou set expires, the request is abandoned, atimeoutevent fires and the status is 0.fetch()has no timeout option of its own, so the usual pattern isAbortSignal.timeout(), which rejects with aTimeoutError. A timeout on the server side is different: a gateway that gives up returns a real 504, which your code receives normally as long as the response carries CORS headers.
Trusted by teams at companies around the world
-
Catch the next outage before your visitors do.
2-minute setup · Cancel any time
-
No credit card needed