Sunday, September 22, 2013

Creating a global http error handler for your AngularJS app

Please note that this is deprecated:

See https://github.com/angular/angular.js/blob/master/CHANGELOG.md#1.1.4 and search for responseInterceptors, its deprecated.

If you're pissed like me about frameworks changing their methods every 6 months don't use Angular!



If you are using AngularJS with REST then it comes in handy to have a global http error handler that will intercept the http status message and display a message on the page accordingly. Therefore I have tweaked and created following error handler based on some examples found on the net.

As you can see the showMessage function will display the messages. The first parameter that can be parsed is the error message, the second parameter is a css class name and the third parameter is the time in miliseconds to display the message.

$httpProvider contains http defaults and is also able to intercept all http responses. So push a function to this interceptor which will listen on http return status messages. If there is a return status message we will trigger a message to be displayed on our Angularjs index.html page.

lib/xx/xx-http-error-handling.js
/**
 * @ngdoc overview
 * @name xx-http-error-handling
 * @description
 *
 * Module that provides global http error handling for apps.
 *
 * Usage:
 * Hook the file in to your index.html: <script src="lib/xx/xx-http-error-handling.js"></script>
 * Add <div class="messagesList" app-messages></div> to the index.html at the position you want to
 * display the error messages.
 */
(function() {
  'use strict';
  angular.module('xx-http-error-handling', [])
    .config(function($provide, $httpProvider, $compileProvider) {
      var elementsList = $();

      // this message will appear for a defined amount of time and then vanish again
      var showMessage = function(content, cl, time) {
        $('<div/>')
          .addClass(cl)
          .hide()
          .fadeIn('fast')
          .delay(time)
          .fadeOut('fast', function() { $(this).remove(); })
          .appendTo(elementsList)
          .text(content);
        };

        // push function to the responseInterceptors which will intercept 
        // the http responses of the whole application
        $httpProvider.responseInterceptors.push(function($timeout, $q) {
          return function(promise) {
            return promise.then(function(successResponse) {
              // if there is a successful response on POST, UPDATE or DELETE we display
              // a success message with green background
              if (successResponse.config.method.toUpperCase() != 'GET') {
                showMessage('Success', 'xx-http-success-message', 5000);
                return successResponse;
              }
            },
            // if the message returns unsuccessful we display the error 
            function(errorResponse) {
              switch (errorResponse.status) {
                case 400: // if the status is 400 we return the error
                  showMessage(errorResponse.data.message, 'xx-http-error-message', 6000);
                  // if we have found validation error messages we will loop through
                  // and display them
                  if(errorResponse.data.errors.length > 0) {
                    for(var i=0; i<errorResponse.data.errors.length; i++) {
                      showMessage(errorResponse.data.errors[i], 
                        'xx-http-error-validation-message', 6000);
                    }
                  }
                  break;
                case 401: // if the status is 401 we return access denied
                  showMessage('Wrong email address or password!', 
                    'xx-http-error-message', 6000);
                  break;
                case 403: // if the status is 403 we tell the user that authorization was denied
                  showMessage('You have insufficient privileges to do what you want to do!', 
                    'xx-http-error-message', 6000);
                  break;
                case 500: // if the status is 500 we return an internal server error message
                  showMessage('Internal server error: ' + errorResponse.data.message, 
                    'xx-http-error-message', 6000);
                  break;
                default: // for all other errors we display a default error message
                  showMessage('Error ' + errorResponse.status + ': ' + errorResponse.data.message, 
                    'xx-http-error-message', 6000);
              }
              return $q.reject(errorResponse);
            });
          };
        });

        // this will display the message if there was a http return status
        $compileProvider.directive('httpErrorMessages', function() {
          return {
            link: function(scope, element, attrs) {
              elementsList.push($(element));
            }
          };
        });
      });
})();
You probably want to style the color of your message. If the message is an error it will be displayed in red. If the message is a success it will be displayed in green.

css/xx-http-error-handling.css
/* display error message styled in red */
.xx-http-error-message {
    background-color: #fbbcb1;
    border: 1px #e92d0c solid;
    font-size: 12px;
    font-family: arial;
    padding: 10px;
    width: 702px;
    margin-bottom: 1px;
}

.xx-http-error-validation-message {
    background-color: #fbbcb1;
    border: 1px #e92d0c solid;
    font-size: 12px;
    font-family: arial;
    padding: 10px;
    width: 702px;
    margin-bottom: 1px;
}

/* display success message styled in green */
.xx-http-success-message {
    background-color: #adfa9e;
    border: 1px #25ae09 solid;
    font-size: 12px;
    font-family: arial;
    padding: 10px;
    width: 702px;
    margin-bottom: 1px;
}
Now all that is left is to add the tag defined by the directive to your index.html file.

index.html
<!doctype html>
<html lang="en" ng-app="myApp">
  <head>
    <meta charset="utf-8">
    <title>YourApp</title>
    <link rel="stylesheet" href="css/app.css"/>
    <link rel="stylesheet" href="css/style.css"/>
    <link rel="stylesheet" href="css/xx-http-error-handling.css"/>
  </head>
  <body>

    <!-- Display top tab menu -->
    <ul class="menu">
      <li><a href="#/user">Users</a></li>
      <li><a href="#/vendor">Vendors</a></li>
      <li><xx-logout-link/></li>
    </ul>

    <!-- Display errors from xx-http-error-handling.js -->
    <div class="http-error-messages" http-error-messages></div>

    <!-- Display partial pages -->
    <div ng-view></div>

    <!-- Include all the js files. In production use min.js should be used -->
    <script src="lib/jquery203/jquery-2.0.3.js"></script>
    <script src="lib/angular114/angular.js"></script>
    <script src="lib/angular114/angular-resource.js"></script>
    <script src="lib/xx/xx-http-error-handling.js"></script>
    <script src="js/app.js"></script>
    <script src="js/services.js"></script>
    <script src="js/controllers.js"></script>
    <script src="js/filters.js"></script>
  </body>
</html>
Last but not least you'll have to hook the module in to your app module so that it will be noticed by AngularJS.

js/app.js
angular.module('myApp', ['xx-http-error-handling'])
  // Define routing
  .config(['$routeProvider', function ($routeProvider) {
    $routeProvider.when('/login', {templateUrl: 'partials/login.html'});
    $routeProvider.when('/user', {templateUrl: 'partials/user.html', controller: UserCtrl});
    $routeProvider.when('/vendor', {templateUrl: 'partials/vendor.html', controller: VendorCtrl});
    $routeProvider.otherwise({redirectTo: '/login'});
  }]);
Here is the JSON output of an error message in case there was an error. As you can see there is an "errors" array that will display validation errors. If there are validation errors and the status is 400 then the validation errors will be displayed each by a single "showMessage" function.

Basically the validation errors should never pop up because we check the validation on client side already. But it is easy for someone to bypass the client side validation and therefore the validation also has to happen on the server side. The messages can help you in case you have forgotten to implement a client validation method. You will then be presented with the error from the server side.

{
  "status":400,
  "code":0,
  "message":"Bad Request",
  "developerMessage":"Validation failed for argument at index 0 in method: ...",
  "moreInfo":null,
  "errors":["Firstname can't be empty", "E-Mail Adress is invalid"]
}

Creating re-usable libraries for your project with Angularjs

There are several ways to structure your code in AngularJS and there is no right or wrong. When you start working with AngularJS I assume that you start by checking out the Angular Seed Project or using Yeoman. However at some point you will want to create libraries or widgets that you want to store in one place for re-use in other projects. For example the "lib" directory.

Examples could include:
  • Global error handler (lib/xx/xx-global-error-handler.js)
  • Authorization (lib/xx/xx-authorization.js)
  • File Upload (lib/xx/xx-file-upload.js)
You can then include these files in to your index.html like this:
<script src="lib/angular.js"></script>
<script src="lib/xx/xx-global-error-handler.js"></script>
<script src="lib/xx/xx-authorization.js"></script>
<script src="lib/xx/xx-file-upload.js"></script>
It is considered as good practice to use a two letter or more letter prefix. For example the starting letters of your surname and name or the first and last letter of your company name. In my case displayed as "xx". This helps to prevent clashes with other files and variables or names you use in your project. I also add the prefix to the name of my controllers, directives and variables.

lib/xx/xx-my-module-name.js
/**
 * @ngdoc overview
 * @name xx-my-module-name
 * @description
 *
 * Your module description goes here
 */
(function() {
  'use strict';
  angular.module('xx-module-name', [])
    .config(['$http',function($http) {
    }])
    .controller('XXMyCtrl', ['$http',function($http) {
    }])
    .directive('xx-my-directive', ['$http', function($http) {
    }]);
})();
Once your module is finished and you added it to the index.html file you'll also have to link it to your main app.js.
angular.module('myApp', ['xx-module-name'])
  // Define routing
  .config(['$routeProvider', function ($routeProvider) {
    $routeProvider.when('/login', {templateUrl: 'partials/login.html'});
    $routeProvider.when('/user', {templateUrl: 'partials/user.html', controller: UserCtrl});
    $routeProvider.when('/vendor', {templateUrl: 'partials/vendor.html', controller: VendorCtrl});
    $routeProvider.otherwise({redirectTo: '/login'});
  }]);
Like this you can start to plugin your own library sets as required.

General Tips on structuring your code

Personally I have started to also put normal code in to a similar fashion. If I for example need code to handle a Vendor then I'll put everything such as controllers, directives, filters and so on that are related to the vendor in to a file called vendor.js. First of all it is very easy to find what you are looking for and you don't fill up a file with several different controllers or directives in which you have to search for the right controller or directive again.