How to Detect Cross Origin (CORS) Error vs. Other Types of Errors for XMLHttpRequest() in Javascript

48,432

Solution 1

No, there is no way to tell the difference, according the W3C Spec.

Here's how the CORS specification specifies the simple cross-origin request procedure:

Apply the make a request steps and observe the request rules below while making the request.

If the manual redirect flag is unset and the response has an HTTP status code of 301, 302, 303, 307, or 308: Apply the redirect steps.

If the end user cancels the request: Apply the abort steps.

If there is a network error: In case of DNS errors, TLS negotiation failure, or other type of network errors, apply the network error steps. Do not request any kind of end user interaction...

Otherwise: Perform a resource sharing check. If it returns fail, apply the network error steps...

In the case of either a failed network connection or a failed CORS exchange, the network error steps are applied, so there is literally no way to distinguish between the two cases.

Why? One benefit is that it prevents an attacker from inspecting the network topology of a LAN. For example, a malicious Web page script could find the IP address of your router by requesting its HTTP interface and therefore learn a few things about your network topology (e.g., how big your private IP block is, /8 or /16). Since your router doesn't (or shouldn't) send CORS headers, the script learns absolutely nothing.

Solution 2

Maybe in case it helps anyone... one other way to handle difference between cors and network error... can work with chrome or firefox... (not perfect solution though)

var external = 'your url';

if (window.fetch) {
    // must be chrome or firefox which have native fetch
    fetch(external, {'mode':'no-cors'})
        .then(function () {
            // external is reachable; but failed due to cors
            // fetch will pass though if it's a cors error
        })
        .catch(function () {
            // external is _not_ reachable
        });
} else {
    // must be non-updated safari or older IE...
    // I don't know how to find error type in this case
}

Solution 3

To differentiate a CORS violation from other failed AJAX requests, you can inspect the response headers of a HEAD request using server-side code and pass the results back to your client page. For example, if the AJAX request fails (status 0), you could call this script (let's call it cors.php) and know for certain if the response headers contain Access-Control-* headers.

Examples:

cors.php?url=http://ip.jsontest.com
cors.php?url=http://www.google.com
cors.php?url=http://10.0.0.1

returns

HTTP/1.1 200 OK Access-Control-Allow-Origin: *
HTTP/1.1 302 Found
Invalid request


cors.php - Customize as needed

<?php /* cors.php */
$url = $_GET["url"];
if(isset($url)) {
    $headers = getHeaders($url);
    header("Access-Control-Allow-Origin: *");

    if(count($headers) == 0) {
        die("Invalid request"); // cURL returns no headers on bad urls
    } else {
        echo $headers[0];       // echo the HTTP status code
    }

    // Include any CORS headers
    foreach($headers as $header) {
        if(strpos($header, "Access-Control") !== false) {
            echo " " . $header;
        }
    }
}

function getHeaders($url, $needle = false) {
    $headers = array();
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 4);        // Timeout in seconds
    curl_setopt($ch, CURLOPT_TIMEOUT, 4);               // Timeout in seconds
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_VERBOSE, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'HEAD');    // HEAD request only
    curl_setopt($ch, CURLOPT_HEADER, true);
    curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($curl, $header) use(&$headers) {
        array_push($headers, $header);
        return strlen($header);
    });
    curl_exec($ch);
    return $headers;
} /* Drakes, 2015 */

Client-side test harness:

function testCORS(url, $elem) {
    $.ajax({
      url: url,
      timeout: 4000
    })
    .fail(function(jqXHR, textStatus) {
       if(jqXHR.status === 0) {
           // Determine if this was a CORS violation or not
           $.ajax({
              context: url,
              url: "http://myserver.com/cors.php?url=" + escape(this.url),
           })
           .done(function(msg) {
              if(msg.indexOf("HTTP") < 0) {
                $elem.text(url + " - doesn't exist or timed out");
              } else if(msg.indexOf("Access-Control-Allow-Origin") >= 0) {
                $elem.text(url + " - CORS violation because '" + msg + "'");
              } else {
                $elem.text(url + " - no Access-Control-Allow-Origin header set");
              }
           });
       } else {
           // Some other failure (e.g. 404), but not CORS-related
           $elem.text(url + " - failed because '" + responseText + "'");
       }
    })
    .done(function(msg) {
      // Successful ajax request
      $elem.text(this.url + " - OK");
    }); /* Drakes, 2015 */
}

Harness driver:

// Create a div and append the results of the URL calls
$div = $("<div>"); 
$("body").append($div);

var urls = ["http://ip.jsontest.com", "http://google.com", "http://10.0.0.1"];
urls.map( function(url) {
   testCORS(url, $div.append("<h4>").children().last());
});

The results:

http://ip.jsontest.com - OK

http://google.com - no Access-Control-Allow-Origin header set

http://10.0.0.1 - doesn't exist or timed out

Solution 4

Surprisingly you can do that for images (and maybe there is a similar solution for other particular content types). The trick is that does apparently not regard CORS, therefore statement "url fails to load by XHR && url succeeds to load by img src" implies that the URL works but is CORS blocked.

This definitely does not help in all cases, but in some cases (e.g. utility detecting if you're properly passing CORS) it may do the trick. For example I wanted to fire a a warning in console if my app (separate frontend and backend) is installed with correctly configured URL of backend and wrongly configured CORS on server, so this was perfectly enough for me as I can serve an image to test it on.

function testCors(url) {
    var myRequest = new XMLHttpRequest();
    myRequest.open('GET', url, true);
    myRequest.onreadystatechange = () => {
        if (myRequest.readyState !== 4) {
            return;
        }
        if (myRequest.status === 200) {
            console.log(url, 'ok');
        } else {
            var myImage = document.createElement('img');
            myImage.onerror = (...args) => {console.log(url, 'other type of error', args);}
            myImage.onload = () => {console.log(url, 'image exists but cors blocked');}
            myImage.src = url;
            console.log(url, 'Image not found');
        }
    };
    myRequest.send();
}

Solution 5

Here's what I did - although it does need you to make changes to the remote server (add a page to it).

  1. I created an probe.html page to be served in an iframe (on the remote origin you are trying to use CORS to contact)
  2. I register a window.onmessage handler in my page
  3. I load the probe.html page into my page using a secure iframe
  4. Within my page on the iframe onload event, I send a message to the iframe using window.postMessage()
  5. The probe.html page probes the url (from the same origin within the iframe)
  6. The probe.html page sends me the xhr.status and xhr.statusText response details using window.postMessage()
  7. If the status is an error, I log the status and statusText to our logging service

However, beware that it is very difficult to make it secure if passing parameters either way (anyone can embed your iframe, other parties can cause postMessage to the page or the iframe).

And it isn't perfect (it only detects errors that are not transient one-offs).

However although it is a lot of work you can get more information about the cause of an error. Very useful when you can't directly access the browser of your users.

PS: this is completely different from the PHP answer that suggests your server should talk to the remote server (a) that isn't much use to diagnose communication failure causes, and (b) using curl from PHP is just asking for your server to be seriously pwned!

Share:
48,432
user2871305
Author by

user2871305

Electrical Engineering background, jack of all trades: Hardware - ASIC, FPGA (VHDL, Verilog, etc.) - uControllers (MIPS, ARM, PIC, etc.) - Analog/digital circuit design (Orcad, spice, etc.) Software - Embedded C (TCP/IP, embedded OS, hardware drivers) - Server side: PHP, Node.JS, MySQL - Client side: Javascript, JQuery, Angular, ... Plus all the other things that startups do: product development, sales, marketing, documentation (Framemaker, Photoshop, Illustrator), web site...

Updated on July 05, 2022

Comments

  • user2871305
    user2871305 almost 2 years

    I'm trying to detect when an XMLHttpRequest() fails due to a Cross Origin Error as opposed to a bad request. For example:

        ajaxObj=new XMLHttpRequest()
        ajaxObj.open("GET", url, true); 
        ajaxObj.send(null);
    

    Consider 4 cases for url:

    Case 1: url is a valid address where access-control-allow-origin is properly set

    • Example: http://192.168.8.35 where I have a server with Access-Control-Allow-Origin: * set in the header
    • This is easy to detect as ajaxObj.readyState==4 and ajaxObj.status==200

    Case 2: url is an invalid address at an existing server

    • Example: http://xyz.google.com where the server responds but it is not a valid request
    • This results in ajaxObj.readyState==4 and ajaxObj.status==0

    Case 3: url is to a non-existing server ip address

    • Example: http://192.168.8.6 on my local network where there is nothing to respond
    • This results in ajaxObj.readyState==4 and ajaxObj.status==0

    Case 4: url is a valid address where access-control-allow-origin is NOT set

    • Example: http://192.168.8.247 where I have a server without Access-Control-Allow-Origin: * set in the header
    • This results in ajaxObj.readyState==4 and ajaxObj.status==0

    The problem is: How do I differentiate Case 4 (access-control-allow-origin error) and Cases 2&3?

    In Case 4, the Chrome debug console shows the error:

    XMLHttpRequest cannot load http://192.168.8.247/. Origin http://localhost is not allowed by Access-Control-Allow-Origin.

    How do I make that error known in Javascript?

    I tried to find some indication in ajaxObj but nothing there seems to be different compared to Case 2&3.

    Here is a simple test I used:

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>CORS Test</title>
    <script type="text/javascript">
    function PgBoot()
    {
    //  doCORS("http://192.168.8.35");   // Case 1
    //  doCORS("http://xyz.google.com"); // Case 2
        doCORS("http://192.168.8.6");    // Case 3
    //  doCORS("http://192.168.8.247");  // Case 4
    }
    
    function doCORS(url)
    {
        document.getElementById("statusDiv").innerHTML+="Processing url="+url+"<br>";
        var ajaxObj=new XMLHttpRequest();
        ajaxObj.overrideMimeType('text/xml');
        ajaxObj.onreadystatechange = function()
        {
            var stat=document.getElementById("statusDiv");
            stat.innerHTML+="readyState="+ajaxObj.readyState;
            if(ajaxObj.readyState==4)
                stat.innerHTML+=", status="+ajaxObj.status;
            stat.innerHTML+="<br>";
        }
        ajaxObj.open("GET", url, true); 
        ajaxObj.send(null);
    }
    </script>
    </head>
    <body onload="PgBoot()">
    <div id="statusDiv"></div>
    </body>
    </html>
    

    Results using Chrome:

    Processing url=http://192.168.8.35
    readyState=1
    readyState=2
    readyState=3
    readyState=4, status=200
    

    Processing url=http://xyz.google.com
    readyState=1
    readyState=4, status=0
    

    Processing url=http://192.168.8.6
    readyState=1
    readyState=4, status=0
    

    Processing url=http://192.168.8.247
    readyState=1
    readyState=4, status=0
    
  • user2871305
    user2871305 over 10 years
    Thanks for the answer. Seems like a flaw in the CORS spec. to me. Not what I wanted to hear but thanks anyway :-)
  • mgold
    mgold about 10 years
    The Chrome debug log shows a very specific error when a CORS request fails. Can we use that / use what it's using?
  • user2871305
    user2871305 over 8 years
    Certainly a server side solution will work because unlike a browser, PHP doesn't care about the Access-Control-Allow-Origin header. However, this solution won't work for me because the request/response is all behind the firewall. The server cannot do a GET request, only the client.
  • Pacerier
    Pacerier over 8 years
    @mgold, Looks like no, at least not currently: stackoverflow.com/q/4844643/632951
  • Pacerier
    Pacerier over 8 years
    @Drakes, Are requesting the server from the browser? If you are doing it from PHP, then it doesn't answer this question.
  • Drakes
    Drakes over 8 years
    @Pacerier Why not? I clearly said to first try an AJAX request (client side), and if it fails (error handler) then to call the cors.php script (again with AJAX, client side).
  • Pacerier
    Pacerier over 8 years
    @Drakes, Mainly folks are trying to do error detection using Javascript alone, but your solution requires the server to act as a proxy.
  • Drakes
    Drakes over 8 years
    @Pacerier, it's either this working solution, or the "No, it's impossible" answer above.
  • apsillers
    apsillers over 8 years
    @mgold No. The browser knows exactly what went wrong, and it tells the user who is physically sitting in front of the computer screen. The browser does not tell the script, which potentially came from a strange website and was written by an unknown person to achieve an unknown (potentially malicious) purpose.
  • apsillers
    apsillers about 7 years
    I tested with fetching https://www.google.com (existing CORS target) versus https://www.google.invalid (nonexistent target) and couldn't see any difference in script-visible behavior. What case is this meant to cover? (Or has the behavior changed since Nov 2016? The spec does change sometimes, especial over matters of CORS sercurity.)
  • Michael
    Michael over 6 years
    So is there no way to distinguish what kind of network error occurred? In a web app, different types of errors may necessitate different actions on the part of the user to recover (e.g. retry again soon versus escalate the issue to someone who shouldn't be bother if the former is appropriate)
  • apsillers
    apsillers over 6 years
    @Michael Correct; network failure and CORS failure (as well as all subtypes of network and CORS failures) are deliberately indistinguishable to scripts reading the XHR API. (The user can look at the console.) You could experimentally try to determine the issue. Can you hit a public, high-availability CORS-enabled resource? It's not a network outage. Can you hit a CORS-public resource on your own domain (which maybe you set up exactly for this test)? It's not a DNS resolution error. Can you load an image from your domain over HTTP but not HTTPS? There might be a TLS negotiation issue.
  • Aaron Blenkush
    Aaron Blenkush over 6 years
    This solution only works with native fetch; it doesn't seem to work with the polyfills.
  • Code Baller
    Code Baller over 5 years
    This works for my use case of simply seeing if a server which doesn't send CORS headers is responding! Luckily that server hosts 1 image. Just wonder if it works in all browsers and if a future update will break it.
  • fnicollet
    fnicollet over 4 years
    This did the trick for me, I was able to catch "Access-Control-Allow-Origin" errors with this snippet. Thanks!