Web API, JavaScript, Chrome & Cross-Origin Resource Sharing

Posted by Brian Lanham on Geeks with Blogs See other posts from Geeks with Blogs or by Brian Lanham
Published on Fri, 02 Nov 2012 14:02:22 GMT Indexed on 2012/11/02 23:01 UTC
Read the original article Hit count: 412

Filed under:

The team spent much of the week working through this issues related to Chrome running on Windows 8 consuming cross-origin resources using Web API.  We thought it was resolved on day 2 but it resurfaced the next day.  We definitely resolved it today though.  I believe I do not fully understand the situation but I am going to explain what I know in an effort to help you avoid and/or resolve a similar issue.

References

We referenced many sources during our trial-and-error troubleshooting.  These are the links we reference in order of applicability to the solution:

Zoiner Tejada

JavaScript and other material from -> http://www.devproconnections.com/content1/topic/microsoft-azure-cors-141869/catpath/windows-azure-platform2/page/3

WebDAV

Where I learned about “Accept” –>  http://www-jo.se/f.pfleger/cors-and-iis?

IT Hit

Tells about NOT using ‘*’ –> http://www.webdavsystem.com/ajax/programming/cross_origin_requests

Carlos Figueira

Sample back-end code (newer) –> http://code.msdn.microsoft.com/windowsdesktop/Implementing-CORS-support-a677ab5d

(older version) –> http://code.msdn.microsoft.com/CORS-support-in-ASPNET-Web-01e9980a

 

Background

As a measure of protection, Web designers (W3C) and implementers (Google, Microsoft, Mozilla) made it so that a request, especially a JSON request (but really any URL), sent from one domain to another will only work if the requestee “knows” about the requester and allows requests from it. So, for example, if you write a ASP.NET MVC Web API service and try to consume it from multiple apps, the browsers used may (will?) indicate that you are not allowed by showing an “Access-Control-Allow-Origin” error indicating the requester is not allowed to make requests.

Internet Explorer (big surprise) is the odd-hair-colored step-child in this mix. It seems that running locally at least IE allows this for development purposes.  Chrome and Firefox do not.  In fact, Chrome is quite restrictive.  Notice the images below. IE shows data (a tabular view with one row for each day of a week) while Chrome does not (trust me, neither does Firefox).  Further, the Chrome developer console shows an XmlHttpRequest (XHR) error.

image image

Screen captures from IE (left) and Chrome (right). Note that Chrome does not display data and the console shows an XHR error.

Why does this happen?

The Web browser submits these requests and processes the responses and each browser is different. Okay, so, IE is probably the only one that’s truly different.  However, Chrome has a specific process of performing a “pre-flight” check to make sure the service can respond to an “Access-Control-Allow-Origin” or Cross-Origin Resource Sharing (CORS) request.  So basically, the sequence is, if I understand correctly: 

1)Page Loads –> 2)JavaScript Request Processed by Browser –> 3)Browsers Prepares to Submit Request –> 4)[Chrome] Browser Submits Pre-Flight Request –> 5)Server Responds with HTTP 200 –> 6)Browser Submits Request –> 7)Server Responds with Data –> 8)Page Shows Data

This situation occurs for both GET and POST methods.  Typically, GET methods are called with query string parameters so there is no data posted.  Instead, the requesting domain needs to be permitted to request data but generally nothing more is required.  POSTs on the other hand send form data.  Therefore, more configuration is required (you’ll see the configuration below).  AJAX requests are not friendly with this (POSTs) either because they don’t post in a form.

How to fix it.

The team went through many iterations of self-hair removal and we think we finally have a working solution.  The trial-and-error approach eventually worked and we referenced many sources for the information.  I indicate those references above.  There are basically three (3) tasks needed to make this work.

Assumptions: You are using Visual Studio, Web API, JavaScript, and have Cross-Origin Resource Sharing, and several browsers.

1. Configure the client

Joel Cochran centralized our “cors-oriented” JavaScript (from here). There are two calls including one for GET and one for POST

function(url, data, callback) {
            console.log(data);
            $.support.cors = true;
            var jqxhr = $.post(url, data, callback, "json")
                .error(function(jqXhHR, status, errorThrown) {
                    if ($.browser.msie && window.XDomainRequest) {
                        var xdr = new XDomainRequest();
                        xdr.open("post", url);
                        xdr.onload = function () {
                            if (callback) {
                                callback(JSON.parse(this.responseText), 'success');
                            }
                        };
                        xdr.send(data);
                    } else {
                        console.log(">" + jqXhHR.status);
                        alert("corsAjax.post error: " + status + ", " + errorThrown);
                    }
                });
        };

The GET CORS JavaScript function (credit to Zoiner Tejada)

function(url, callback) {
            $.support.cors = true;
            var jqxhr = $.get(url, null, callback, "json")
                .error(function(jqXhHR, status, errorThrown) {
                    if ($.browser.msie && window.XDomainRequest) {
                        var xdr = new XDomainRequest();
                        xdr.open("get", url);
                        xdr.onload = function () {
                            if (callback) {
                                callback(JSON.parse(this.responseText), 'success');
                            }
                        };
                        xdr.send();
                    } else {
                        alert("CORS is not supported in this browser or from this origin.");
                    }
                });
        };

The POST CORS JavaScript function (credit to Zoiner Tejada)

Now you need to call these functions to get and post your data (instead of, say, using $.Ajax). Here is a GET example:

corsAjax.get(url, function(data) { if (data !== null && data.length !== undefined) { // do something with data } });

And here is a POST example:

corsAjax.post(url, item);

Simple…except…you’re not done yet.

2. Change Web API Controllers to Allow CORS

There are actually two steps here.  Do you remember above when we mentioned the “pre-flight” check?  Chrome actually asks the server if it is allowed to ask it for cross-origin resource sharing access.  So you need to let the server know it’s okay.  This is a two-part activity.  a) Add the appropriate response header Access-Control-Allow-Origin, and b) permit the API functions to respond to various methods including GET, POST, and OPTIONS.  OPTIONS is the method that Chrome and other browsers use to ask the server if it can ask about permissions.  Here is an example of a Web API controller thus decorated:

NOTE: You’ll see a lot of references to using “*” in the header value.  For security reasons, Chrome does NOT recognize this is valid.

[HttpHeader("Access-Control-Allow-Origin", "http://localhost:51234")]
[HttpHeader("Access-Control-Allow-Credentials", "true")]
[HttpHeader("Access-Control-Allow-Methods", "ACCEPT, PROPFIND, PROPPATCH, COPY, MOVE, DELETE, MKCOL, LOCK, UNLOCK, PUT, GETLIB, VERSION-CONTROL, CHECKIN, CHECKOUT, UNCHECKOUT, REPORT, UPDATE, CANCELUPLOAD, HEAD, OPTIONS, GET, POST")]
[HttpHeader("Access-Control-Allow-Headers", "Accept, Overwrite, Destination, Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, If-Modified-Since, X-File-Name, Cache-Control")]
[HttpHeader("Access-Control-Max-Age", "3600")]
public abstract class BaseApiController : ApiController
{
    [HttpGet]
    [HttpOptions]
    public IEnumerable<foo> GetFooItems(int id)
    {
        return foo.AsEnumerable();
    }

    [HttpPost]
    [HttpOptions]
    public void UpdateFooItem(FooItem fooItem)
    {
        // NOTE: The fooItem object may or may not
        // (probably NOT) be set with actual data.
        // If not, you need to extract the data from
        // the posted form manually.

        if (fooItem.Id == 0) // However you check for default...
        {
            // We use NewtonSoft.Json.
            string jsonString = context.Request.Form.GetValues(0)[0].ToString();
            Newtonsoft.Json.JsonSerializer js = new Newtonsoft.Json.JsonSerializer();
            fooItem = js.Deserialize<FooItem>(new Newtonsoft.Json.JsonTextReader(new System.IO.StringReader(jsonString)));
        }

        // Update the set fooItem object.
    }
}

Please note a few specific additions here:

* The header attributes at the class level are required.  Note all of those methods and headers need to be specified but we find it works this way so we aren’t touching it.

* Web API will actually deserialize the posted data into the object parameter of the called method on occasion but so far we don’t know why it does and doesn’t.

* [HttpOptions] is, again, required for the pre-flight check.

* The “Access-Control-Allow-Origin” response header should NOT NOT NOT contain an ‘*’.

3. Headers and Methods and Such

We had most of this code in place but found that Chrome and Firefox still did not render the data.  Interestingly enough, Fiddler showed that the GET calls succeeded and the JSON data is returned properly.  We learned that among the headers set at the class level, we needed to add “ACCEPT”.  Note that I accidentally added it to methods and to headers.  Adding it to methods worked but I don’t know why.  We added it to headers also for good measure.

[HttpHeader("Access-Control-Allow-Methods", "ACCEPT, PROPFIND, PROPPA...
[HttpHeader("Access-Control-Allow-Headers", "Accept, Overwrite, Destin...

Next Steps

That should do it.  If it doesn’t let us know.  What to do next? 

* Don’t hardcode the allowed domains.  Note that port numbers and other domain name specifics will cause problems and must be specified.  If this changes do you really want to deploy updated software?  Consider Miguel Figueira’s approach in the following link to writing a custom HttpHeaderAttribute class that allows you to specify the domain names and then you can do it dynamically.  There are, of course, other ways to do it dynamically but this is a clean approach.

http://code.msdn.microsoft.com/windowsdesktop/Implementing-CORS-support-a677ab5d

© Geeks with Blogs or respective owner