Tuesday 22 April 2014

JSONP and CORS

JSONP allows GET verbs to be passed across domains. But it is restricted only to GET verbs. Custom or standard headers cannot be added, so you cannot include ETag when calling a REST service to use the cache where possible.

The more modern technique is to use CORS. This works for more modern browsers.

To get CORS to work with AngularJS, first add the code:

var dashboard = angular.module('dashboardApp', []);

dashboard.config(['$httpProvider', function ($httpProvider) {
    $httpProvider.defaults.useXDomain = true;
}
]);

The useXDomain means the browser will send the Origin: http://serverhost header in the request.
The request can now be changed from

$http({
            'method': 'JSONP',
            'url': endurTradeServiceUrl + '?callback=JSON_CALLBACK',
            .....

to

$http({
            'method': 'GET',
            'url': endurTradeServiceUrl,
            .....


However if you look in Firebug, the server still shows no response:


Note that the Content-Length is 715 bytes but firebug shows no data:


This is because the browser's Same Origin policy is working. Now we need to go server-side and add the following header to the response:

public HttpResponseMessage Get()
        {
            var cache = ObjectFactory.GetInstance();

            var response = Request.CreateResponse(HttpStatusCode.OK, cache.Strategies);
            response.Headers.Add("Access-Control-Allow-Origin", "*");
            return response;
        }

And now it will work. Note that the wildcard for the response header is not best practice.

Now for the headers. If we have the code to add custom headers:

$http({
            'method': 'GET',
            'url': endurTradeServiceUrl,
            headers: {
                'if-none-match' : 'Today',
                'mycustom' : 'Bob'
            }
        })

Then go back to Firefox we can see that the client is now calling the OPTIONS method and requesting permission to use the headers with the Access-Control-Request-Headers header. This is the CORS "preflight" request:


Note:

The browser can skip the preflight request if the following conditions are true:

The request method is GET, HEAD, or POST, **and**
The application does not set any request headers other than Accept, Accept-Language, Content-Language, Content-Type, or Last-Event-ID, **and**
The Content-Type header (if set) is one of the following:
 - application/x-www-form-urlencoded
 - multipart/form-data
 - text/plain

As we are sending JSON and custom headers, these conditions are not met and so the preflight request is raised.

Now to handle the preflight:
Install the NuGet Package Microsoft.AspNet.WebApi.Cors.

Add the following code to WebApiConfig.cs

public static void Register(HttpConfiguration config)
        {
            var enableCorsAttribute = new EnableCorsAttribute("*", "*", "GET, PUT, POST, DELETE, OPTIONS"); 
            config.EnableCors(enableCorsAttribute);

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }

and you can then remove the lines
response.Headers.Add("Access-Control-Allow-Origin", "*");

from the controller actions.
The server
is now handling the Options message:

You may wish to read a header from the server. This may be useful for example if you want to store the ETag and reuse it for subsequent requests, thus using the full caching capability. If you want to be able to read a header from the server, the server needs to add the line

response.Headers.Add("Access-Control-Expose-Headers", "ETag");

NOTE: You CANNOT use the wildcard ("*") for Access-Control-Expose-Headers - it is rejected by Firefox and Chrome. It has to be a comma-separated list of headers.

References
Wikipedia
MSDN Blogs
CORS support in ASP.NET

No comments:

Post a Comment