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"]
}

6 comments:

  1. saludos ... genial el tutorial pero tengo un error... puedes subir el codigo.

    ReplyDelete
    Replies
    1. Hi Jorge,

      I don't speak Spanish I'm sorry. If you got an error you can post the error if you want and I can take a look at it and maybe point you in to the right direction. Please note that I don't have the time to debug your code. I've written this tutorial because it took me weeks to figure out how it works :)

      Kind regards,
      Chris

      Delete
  2. Great solution but it is now not working with AngularJS 1.3. What is the alternative to achieve this functionality?

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

      Delete
    2. Thanks, the problem with all these frameworks is that they are not stable. They reinvent all their functions from one release to the other and the developer has to adjust if he wants to or not. This causes too much work for adjustments, a lot of bugs and drives customers crazy when you tell them how expensive it is to upgrade. This is why I personally switched back to jQuery.

      Delete
  3. I created an angular 1.3 version of this for when it's finally released. Hope this helps anybody looking for it. https://gist.github.com/dmdevoss/19659da45eb12ce462f3

    ReplyDelete