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 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 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 a TypeError that reads Failed to fetch, NetworkError when attempting to fetch resource. or Load 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.

YouDNSNetworkCDN / ProxyWeb serverApp / DB
The path a request takes from your browser to the website's servers. A status 0 failure is produced at the highlighted stages.
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:

What a status 0 failure looks like in the browser. The exact wording varies by browser, device, and server.
  • xhr.status === 0 XMLHttpRequest after a network error, an abort or a CORS failure
  • TypeError: Failed to fetch Chrome and Edge, when fetch() rejects
  • TypeError: NetworkError when attempting to fetch resource. Firefox
  • TypeError: Load failed Safari
  • AbortError fetch() cancelled through an AbortController
  • net::ERR_BLOCKED_BY_CLIENT Chrome console, when an extension blocked the request
  • CORS error Status column of the Chrome DevTools Network panel
  • status: 0, statusText: "error" jQuery $.ajax and 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 seeLikely causeWhere the fix goes
Console: blocked by CORS policyThe response had no matching Access-Control-Allow-Origin, or the preflight failedThe server or the proxy in front of it
net::ERR_BLOCKED_BY_CLIENTAn ad blocker or privacy extension matched the URLThe visitor's browser, or a less tracker-like URL
Status column: (canceled)A navigation, an abort() call, or a newer request replaced itUsually nowhere: filter it out of error reports
(blocked:mixed-content)An HTTPS page requested an http:// URLServe the endpoint over HTTPS
net::ERR_CERT_ or ERR_SSL_ codesThe TLS handshake failedThe certificate or TLS setup
net::ERR_NAME_NOT_RESOLVED, ERR_CONNECTION_REFUSEDDNS or the connection failed before HTTP beganDNS, the firewall, or the server itself
net::ERR_INTERNET_DISCONNECTEDThe visitor went offlineNowhere server-side; retry when back online

What causes HTTP Status 0?

  • A CORS failure. The server answered, but without an Access-Control-Allow-Origin header matching the calling page, so the browser discarded the response. A preflight OPTIONS request 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, track or ads.
  • The request being cancelled: the visitor navigated away or closed the tab, your code called abort(), or a timeout you set on an XMLHttpRequest expired 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-cors or manual-redirect fetch, both of which return status 0 by design rather than because anything failed.

How to find the cause fast

  1. 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 a net::ERR_ code naming the network failure.
  2. Send the same request with curl, including the Origin header your page sends. A normal response with no Access-Control-Allow-Origin header in it confirms CORS as the cause.
  3. Try a private window with extensions turned off. If the request succeeds there, an extension was blocking it.
  4. 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.
What a status 0 failure looks like from the command line. The grey lines starting with # are explanatory comments.

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

  1. Open DevTools and read the console before changing anything. The browser keeps the reason from your script but prints it there.
  2. 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.
  3. 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.
  4. 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.
  5. Retry idempotent requests once the connection returns, and treat navigator.onLine as a hint rather than a guarantee, since it can report online on a network that goes nowhere.

If you run the server or API

  1. Send Access-Control-Allow-Origin for every origin that should call you, and answer the OPTIONS preflight with a 2xx that includes Access-Control-Allow-Methods and Access-Control-Allow-Headers.
  2. 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 without always the header is left off 4xx and 5xx responses.
  3. For requests that carry cookies or credentials, return the exact origin rather than * and add Access-Control-Allow-Credentials: true, since browsers reject a wildcard on credentialed requests.
  4. Keep a valid certificate and serve the API over HTTPS only, so mixed-content blocking and TLS failures never reach visitors.
  5. 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.status after a network error or an abort, and the status of the opaque responses that no-cors and 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-Origin header 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 the OPTIONS preflight 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_CLIENT in the console when this happens. Paths containing words like analytics, track or ads are 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 a TypeError, and an aborted request rejects with an AbortError. The only responses fetch resolves with a status of 0 are opaque ones, from no-cors requests 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-Origin and is discarded like any other CORS failure. In nginx, add_header leaves error responses out unless you add the always parameter. 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 generic TypeError, 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 with mode: "no-cors": a response whose type is opaque, 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 the timeout you set expires, the request is abandoned, a timeout event fires and the status is 0. fetch() has no timeout option of its own, so the usual pattern is AbortSignal.timeout(), which rejects with a TimeoutError. 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