Use Angular $http Interceptors

When working with HTTP API of an backend, you need to handle various response from those APIs and make your own reaction on either success or failure response. But those APIs are not always designed RESTfully, It may not indicate an error status using the status code of HTTP. Instead, it may use an special field in the response body to indicate a status of an response. This implicate an burden on the developer who will not be able to use the facility provided by the framework like $http. Normally, $http() would return an promise object which can be chained with then method to handle both success or failure response. But when an HTTP request doesn’t using HTTP status code to indicate its result, your error handlers will never be called. In this case, you may need to check the response entity carefully to see whether its special field indicates its failure status.

I’v also encountered this issue when joining a new team. And I think I can’t change the API definition. So the only way to save time is do some tricky. And this issue can be easily solved by using $http interceptors.

Assume that I have an API which return an object like this.

1
2
3
4
5
6
{
"status": 0, // 0 indicates an successful request, otherwise the request is a failure one.
"data": {
}, // this is what the real data resides in.
"message": null // If status isn't 0, this message will contain the error information.
}

Well, this API definition is very straightforward though it is not follow the RESTful style. What I need do is to intercept every response made from this API and check the value of status. If it is not 0, change the statusCode of the response object to >=400. Besides, that I also want to make an global interceptor to achieve this so I can get rid of writing same check code everywhere.

Let’s start to see what $http interceptors can help.

According to AngularJS Document Interceptors are factory which can modify request and response. You need define some interceptors and push them into $httpProvider.interceptors array in your config function.

Let’s define an interceptor to check the status and reject the response if status code is not 0. Assume we have an API pattern is /api/:someTarget (:someTarget is an parameter which can be changed by need).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var restInterceptor = angular.module('RestInterceptor', []);

restInterceptor.factory('RestInterceptor', function($q) {
return {
// response function will be invoked when request responds with successful status.
// This is usually means the status code is 200~299 or an redirect response
response: function(response) {
var apiPattern = /^(?:\/api\/)\S+/;
// we need filter the config object to ensure only check our API response.
if(apiPattern.test(response.config.url) && typeof response.data.status !== 'undefined' && response.data.status !== 0) {
//here we modify the http status code to 400 indicates this should be treated as an error.
response.status = 400;
// reject this response will let the end point caller's error handler be called. also, you can
// chain an responseError interceptor to handle this error.
return $q.reject(response);
}

return response;
}
}
});

This code snippet create an interceptor to check the normal response whose HTTP status code is usually 200, but modify an response if its data.status doesn’t equal 0. and modify the HTTP status code to 400. so we can treat the API as an RESTful API and All the facility provided by angular or three party dependency can be used directly without redundant check code.

The rest job is to make the interceptor work, we need to push this factory into $httpProvider.interceptors. Note that the invoke order of response and responseError interceptors are in reverse registration order. This is not clearly documented in official document.

1
2
3
4
5
6
var app = angular.module('myApp', ['RestInterceptor']);
app.config(function($httpProvider, RestInterceptor) {
// if you have other interceptors to handle error response, make sure to push "RestInterceptor" at last.
$httpProvider.interceptors.push('OtherErrorHandlerInterceptor');
$httpProvider.interceptors.push('RestInterceptor');
})

Now you can write your code without need to check whether you got an error in your success handler.

1
2
3
4
5
6
7
8
9
10
11
12
var app = angular.module('myApp');
app.controller('SomeController', function($scope, $http){
$http.get('/api/first-item')
.success(function(data, status, headers, config) {
// write your code for successful request
// result object will be always valid.
console.log(data);
})
.error(function(data, status, headers, config) {
console.log(status); // will print 400 when data.data.status != 0
});
});

Next time, I will use interceptor to write a notification module which will automatically make a toast notification to user when an API request has failed. And will combine with the RestInterceptor I just have wrote.

Comments

Beware Of Using value Attribute On list element

In AngularJs development, we use custom attribute in our directive is nontrivial things. you can even use some attributes which may already be defined in W3C standards of that element. well, this doesn’t matter at most circumstance. But it is not recommended to override the meaning of those attribute originally defined by W3C. I have paid for this.

I have a directive nya-bootstrap-select which using a value attribute on list element. I give this attribute a new meaning that its value will be the option value for predefined options. It works well in AngularJS apps until I tried to add some e2e test on it using Protractor.

In Protractor elements are selected by the webdriver api and I hasn’t look into the implementation of selenium, but I can sure it may take the W3C standard and make its own implementation to retrieve the value of value attribute.

As the W3C standards says:

This integer attributes indicates the current ordinal value of the item in the list as defined by the <ol> element. The only allowed value for this attribute is a number, even if the list is displayed with Roman numerals or letters. List items that follow this one continue numbering from the value set. The value attribute has no meaning for unordered lists (<ul>) or for menus (<menu>).

The list element has a value attribute which only accepts integer value. Although I can use a String value in my AngularJS app but this is not warranted by standard and can varies between implementations. Just as the selenium implementation, the WebElement.getAttribute(‘value’) on my directive always return 0. This is not my expected.

In conclusion, It is recommended for those who want to use some predefined attribute on their own directive to look up the standard whether its behavior has already been defined to avoid conflicts and variants between implementations.

Comments

Busting The Cache

Last week, I completed my new hexo theme, but newly added page were broken because the stylesheet were not updated with the page. This was caused by the cache either on browser side or on the CDN of github pages side. So I decided to solve this issue by using a method to busting the cache whenever I have updated the assets files.

If anyone have ever used yeoman to generate an angular project. You may find a grunt task in the generated Gruntfile called usemin. This task combined with several other task can concatenate your stylesheet and javascript files then minimize those files, at last a file revision task is executed internally. The key of busting cache is the file revision task which will calculate your files’ hash value and use the value to rename your file, this will totally solve the cache issue. whenever your file changes, the hash value changes and you get a different filename of assets.

Back to the hexo theme project, I found it is hard to use the grunt task directly in my project because those task will modify the html or template file to update the script and link tag in order to revision the assets file name. But hexo template file should only be modifed by programmer and generate html file by hexo. So I think it’s time to write a similar procedure by myself using hexo api.

Hexo provide two api helper and generator to generate tags in template and generate files with specified path. I will use this two api to keep the reference in template update and generate the new file with hash prefix file name. Because I can’t ensure the execution order of these two api, the concat and minified operation are abandoned. First let’s write a file hash method. This method accepts two arguments: file absolute path and hash digit length. because we don’t need the entire hash string to name asset file. the return value will be the new file name prefixed with a hash string

1
2
3
4
5
6
7
8
9
var fs = require('fs'),
path = require('path'),
crypto = require('crypto');
// modules above will be shared in the following two code blocks.
function filehash(filepath, digitlength) {
var hash = crypto.createHash('md5').update(fs.readFileSync(filepath)).digest('hex');
var prefix = hash.slice(0, digitlength);
return prefix + '.' + path.basename(filepath);
}

This method will be called by helper and generator to get the assets files’ hash value file name.

Now let’s begin from the generator. this api will let me to provide an array which tell hexo to write some files with specified path. I can copy the assets file with hashed file name.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var themeSourcePath = path.resolve(__dirname, '../source'); // we need the theme source path to find the assets folder
var hashLength = 8;
// this is define of the generator.
hexo.extend.generator.register('filerev', function(locals) {
// find all directories in theme source directory.
var directories = fs.readdirSync(themeSourcePath);
var outputData = [];
directories.filter(function(dir){
return dir === 'css' || dir === 'js';
}).forEach(function(dir){
var files = fs.readdirSync(path.join(themeSourcePath, dir));
outputData = outputData.concat(files.map(function(file) {
return {
path: dir + '/' + filehash(path.join(themeSourcePath, dir, file), hashLength), // call filehash method we write before.
data: function() {
return fs.createReadStream(path.join(themeSourcePath, dir, file));
}
};
}));
});

return outputData;
});

This generator will generate a set of new file with the hash prefix and its original folder structure in public folder when you run hexo generate.

With the hash prefixed file generated properly, we have to update the reference in script and link tag. here we using helper to define an ejs helper called usemin to generate our script and link tag.

1
2
3
4
5
6
7
8
hexo.extend.helper.register('usemin', function(input) {
var filepath = path.join(themeSourcePath, input);
var dir = input.split('/');
var ext = path.extname(input);
var newFilename = filehash(filepath, hashLength);
var newPath = path.resolve('/', path.join(dir.slice(0, dir.length - 1).join('/'), newFilename));
return ext === '.js' ? '<script type="text/javascript" src="' + newPath + '"></script>' : '<link rel="stylesheet" href="' + newPath + '">';
});

Put this scripts file in scripts folder under my theme directory, the script will be loaded by hexo automatically. Now there are only one step to get the goal, modify template using the usemin helper

In head.ejs

1
2
3
4
5
<head>
<!-- other tags... -->
<%- usemin('css/styles.css') %>
<%- css('fancybox/source/jquery.fancybox.css') %>
</head>

In scripts.ejs

1
2
3
4
<!-- other tags... -->
<%- usemin('js/jquery.js') %>
<%- js('fancybox/source/jquery.fancybox.js') %> <!-- we can't process this file, so use the js helper -->
<%- usemin('js/caption.js') %>

When I run hexo generate, the index.html will be generated by hexo which contains head.ejs and scripts.ejs part

1
2
3
4
5
6
7
8
9
10
11
<head>
<!-- omit other tags... -->
<link rel="stylesheet" href="/css/2bb849c6.styles.css">
<link rel="stylesheet" href="/fancybox/source/jquery.fancybox.css">
</head>
<body>
<!-- omit other tags... -->
<script type="text/javascript" src="/js/cf26f8f0.jquery.js"></script>
<script src="/fancybox/source/jquery.fancybox.js" type="text/javascript"></script>
<script type="text/javascript" src="/js/33415e49.caption.js"></script>
</body>
Comments

Fix Theme bug in Safari

I found my new theme behaves strange in iOS. After an inspect, I found this was caused by missing the vendor prefix of some css property.

From caniuse.com I know that the 3d transform property although is supported by Safari and iOS, but it is still needed a vendor prefix to make it work. After add the -webkit- prefix. the bug on iOS disappeared. For convenience, I using a grunt task grunt-autoprefixer to do this automatically.

Comments
OLDER POSTS