Apri 27th, 2014
Technologies stack:
- AngularJS — Superheroic JavaScript MVW Framework
- Restangular
- Web API : The Official Microsoft ASP.NET Site
Tools:
- WebStorm :: The smartest JavaScript IDE - JetBrains
- Chrome Web Store - Dev HTTP Client
- Visual Studio - Home
- Fiddler - The Free Web Debugging Proxy by Telerik
Context:
I’m working on a small SPA (single page application) that will consume a web API (also build in house ) that will be hosted on a different site than where the SPA is being served from.
Taking the highest priority feature, I decided to build it out vertically through the technology stack instead of first building out the entire web service and then SPA. By doing so, it helps me discover issues between client and service early on before I go too far in the wrong direction and not to mention it keeps me focused on the priority of the features.
Knowing that the web API will be hosted on a separate site, I enabled cross origin request globally as follows:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var cors = new EnableCorsAttribute("*", "*", "*");
config.EnableCors(cors);
// ...
}
}
{
public static void Register(HttpConfiguration config)
{
var cors = new EnableCorsAttribute("*", "*", "*");
config.EnableCors(cors);
// ...
}
}
With the first action returning data to the expected request using Chrome Dev HTTP client tool. I then turned my attention to the SPA and built out a request to the abovementioned action. (omitting a lot of details for conciseness). Surprisingly, I ran into an issue when the SPA made a request to the web API and it was returning HTTP status 500 (server internal error).
The Environment:
Before I go to describing the problem, let me share my dev environment:
The SPA:- My spa is build on AngularJS framework with Restangular as means to call the API. The project resides in WebStorm and it is running on: http://localhost:63342/Velocity/www/index.html
- The API is built using ASP.NET Web API 2 in Visual Studio. It is running on http://localhost:63200/api
The Issue:
As I mentioned, when Restangular calls the API’s action, it is returning a status 500 indicating that the service just blew up. Below is my Angular factory and controller that makes the call to the API:
angular.module(Velocity.services', [])
.factory('Calls', ['$http', 'Settings', 'Restangular',
function ($http, Settings, Restangular) {
console.log(Velocity.services.calls');
var config = {};
Settings.get(function (setting) {
config = setting;
});
var calls = [];
return {
all: function(parseCalls, failedHandler, refresh) {
this.setHeaders();
Restangular
.one("GetAll")
.get({ 'refresh': refresh })
.then(function(response) {
if (typeof response !== 'undefined' && response) {
if (response.Success) parseCalls(response.ServiceCalls);
else alert(response.Message);
}
},
function(reason) {
failedHandler(JSON.stringify(reason));
}
);},
setHeaders: function() {
var httpConfig = {
headers: {
'Token': config.token,
'Accept': 'application/json'
}
};
Restangular.setBaseUrl(config.host + '/ServiceCalls');
Restangular.setDefaultHeaders(httpConfig.headers);
}
};
}])
.controller('CallsCtrl', function ($scope, $state, Calls) {
$scope.calls = [];
$scope.initialized = false;
$scope.init = function () {
if (this.initialized) return;
this.getCallList(false);
$scope.initialized = true;
};
$scope.getCallList = function (refresh) {
Calls.all(
//success
function (calls) {
$scope.calls = calls;
},
//fail
function (message) {
alert("Failed to retrieve data from server: \r" + message);
},
//refresh
true
);
};
$scope.init();
});
To see what in the world is going on over the wire, I turned on Fiddler to watch the traffic. To my surprise, I found whenever the SPA made the call to get data, the browser is sending an HTTP command “OPTIONS” instead of the expected ‘GET’ as follows:
OPTIONS http://localhost:63200/api/ServiceCalls/GetAll?refresh=false HTTP/1.1
Host: localhost:63200
Connection: keep-alive
Cache-Control: no-cache
Pragma: no-cache
Access-Control-Request-Method: GET
Origin: http://localhost:63342
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.116 Safari/537.36
Access-Control-Request-Headers: accept, token
Accept: */*
Referer: http://localhost:63342/Velocity/www/index.html
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
After reading some posts and articles online, it turned out that the browser was asking API service for permission to use two unknown headers: accept and token. It is blowing up the API because it is not handling the OPTIONS command at all.
The solution is then to fulfill the access control request.
The Solution:
Asp.Net Web API provides this nice facility called HTTP message handler that can be used to intercept the request and handle the HTTP ‘Options’ command properly.
public class MessageHandler1 : DelegatingHandler
{
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//if the command is: 'OPTIONS'
if (request.Method == HttpMethod.Options)
{
var response = request.CreateResponse(HttpStatusCode.OK);
//allow cross origin
response.Headers.Add("Access-Control-Allow-Origin", "*");
//allow all http methods
response.Headers.Add("Access-Control-Allow-Methods", "*");
//below the two headers that I want to allow
response.Headers.Add("Access-Control-Allow-Headers", "token");
//age of the access control
response.Headers.Add("Access-Control-Max-Age", "360000");
return await Task.Factory.StartNew(() => response, cancellationToken);
}
//otherwise let the request pass through
return await base.SendAsync(request, cancellationToken);
}
This message handler needs to be registered with the asp.net global configuration:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MessageHandlers.Add(new MessageHandler1());
}
}
{
public static void Register(HttpConfiguration config)
{
config.MessageHandlers.Add(new MessageHandler1());
}
}
Now when I run the SPA, I see the browser making two requests, first with the ‘OPTIONS’ command and it receives from the server the following response granting the SPA to use this new header and access with cross origin. The following is the response to the OPTIONS request:
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Server: Microsoft-IIS/8.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: *
Access-Control-Allow-Headers: token
Access-Control-Max-Age: 360000
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?QzpcVEZTLVJSLVRGUzAxXE9tZWdhXERldlxPbWVnYS5OZXRcT21lZ2EuTmV0LldlYkFwaVxhcGlcU2VydmljZUNhbGxzXEdldEFsbA==?=
X-Powered-By: ASP.NET
Date: Sun, 27 Apr 2014 22:13:44 GMT
Content-Length: 0
The second request is the actual API request and it returns the data as expected:
GET http://localhost:63200/api/ServiceCalls/GetAll?refresh=false HTTP/1.1
Host: localhost:63200
Connection: keep-alive
Cache-Control: no-cache
Pragma: no-cache
Accept: application/json
Origin: http://localhost:63342
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.116 Safari/537.36
Token: 1a2b7f586-32f2
Referer: http://localhost:63342/Velocity/www/index.html
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Thank you very much, it helps me a lot.
ReplyDelete