The Lanham Factor
The (ir)rational thoughts of a (not-so)mad man

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

Friday, November 2, 2012 4:02 PM

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.

My Lotic Factor colleagues Joel Cochran and Chris Atienza suffered with me and Joel wrote an excellent, complementary blog post to this post.

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 (window) {
    function CorsAjax() {
        this.post = function(url, data, callback) {
            $.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 {
                        logger.write(">" + jqXhHR.status);
                        alert("corsAjax.post error: " + status + ", " + errorThrown);
                    }
                });
        };

        this.get = 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 {
                        logger.write(">" + jqXhHR.status);
                        alert("corsAjax.get error: " + status + ", " + errorThrown);
                    }
                });
        };
    };

    window.corsAjax = new CorsAjax();
})(window);

The GET & PUT CORS JavaScript functions (credit to Zoiner Tejada, Joel Cochran)

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




Feedback

# re: Web API, JavaScript, Chrome & Cross-Origin Resource Sharing

Thinktecture.IdentityModel has a fully functional spec-compliant open source CORS implementation here (I'm sure it would have saved you some headache):

http://brockallen.com/2012/06/28/cors-support-in-webapi-mvc-and-iis-with-thinktecture-identitymodel/
11/2/2012 8:28 PM | Brock Allen

# re: Web API, JavaScript, Chrome & Cross-Origin Resource Sharing

Brock,

I saw that come up several times. I unfairly avoided it because a) learning curve and b) figured there must be a "quick" solution. The more we dug into it the worse it got. The thing that killed us was the part where we missed the "ACCEPT" header. I will take a look at your implementation however. If for no other reason than my own edification but hopefully to use it. 11/2/2012 9:21 PM | Brian C. Lanham

# re: Web API, JavaScript, Chrome & Cross-Origin Resource Sharing

Thank you for such an awesome CORS implementation. I had problems with Thinktecture.IdentityModel because I am running in IIS 6 but your implementation is working in firefox and chrome. In IE, there is a javascript error in:
xdr.send(data); line
Error: Invalid argument.

why do you think xdr.send(data) is not working in IE but working in Chrome and Firefox.

I am using JSON.parse(fakeData) to parse the data.

Anybody's feedback will be greatly appreciated.

1/16/2013 12:40 PM | Sanjiv Lamsal

# re: Web API, JavaScript, Chrome & Cross-Origin Resource Sharing

Sanjiv, I will ask Joel to help me look into this and see if we can identify something that may help you. Is your "fakeData" in JSON format? Are you using Fiddler to inspect your transmissions? If not you really need to get Fiddler and see the details behind the calls. 1/17/2013 8:30 AM | Brian C. Lanham

# re: Web API, JavaScript, Chrome & Cross-Origin Resource Sharing

Brian,

Thank you for your quick response. I am using Fiddlers to do inspection. It is an awesome tools to have. My "fakedata" is in JSON format. That's one of the reason it works in Chrome and Firefox. I still havenot figured out why it is not working in IE..to be specific IE 10. 1/17/2013 12:16 PM | Sanjiv Lamsal

# re: Web API, JavaScript, Chrome & Cross-Origin Resource Sharing

I ran into this issue and quite frankly, I cheated. I created a WCF service endpoint with webHttpBinding. The client (IE, FF, Chrome), fires an AJAX POST request to a LOCAL service endpoint. I take the stringifyed parameter info to the web method and carry it into my biz logic. From there I issue a Restsharp (104.1 net) request:
RestRequest request = new RestRequest();
request.AddHeader(ANYHTING I WANT - SECURITY STUFF FOR EXAMPLE);

I control everything at this point - in terms of the request AND the response. With the request, I execute as a GET, POST, PUT, DELETE. With the response, I do some deserialization, JSON data cleanup, parsing - whatever - and return to service endpoint and ultimately to the client with data to markup.

This technique works in IE, FF, Chrome.

I have to admit, it was a real head-scratcher and does not even come close to the elegant work around that I read in this blog.

-Your humble heads-down coder,
Mike DiRenzo, Charlotte, NC, USA
3/6/2013 10:27 AM | Mike DiRenzo

# re: Web API, JavaScript, Chrome & Cross-Origin Resource Sharing

Great post - helped me a lot!

Couple of questions:

Have you been able to get this to work when the origin url is different than the webapi url? E.g. origin (app.foo.com) and webapi (api.foo.com or api.bar.com)? I can only get this to work if both are the same but can have different ports.

Also, in either case I can't get FF to pass credentials, i.e. cookies - is this even possible?

Thanks!! 3/14/2013 12:04 AM | Raman

# re: Web API, JavaScript, Chrome & Cross-Origin Resource Sharing

Raman,

Yes. This is how we are using it. For example, we have "foo.cloudapp.net" talking to both "foo.cloudapp.net:8080" (with /api) as well as "anotherapp.cloudapp.net". Does that make sense?

Can you post a code snippet? Maybe I can help. 3/25/2013 7:16 PM | Brian Lanham

# re: Web API, JavaScript, Chrome & Cross-Origin Resource Sharing

Mike,

Nothing wrong with "cheating". The important point is to get it to work. Afterwards you can deal with specific implementation details. Your solution is definitely interesting 3/25/2013 7:18 PM | Brian Lanham

Post a comment