AngularJS: Wrapping $http For Fun and Profit

August 10th, 2013 Permalink

AngularJS runs its XHR HTTP requests through the $http service. It's a simple interface, easily used in your own services:

// Create an example module.
var example = angular.module('example', []);

/**
 * An example service definition.
 */
function exampleService($http) {
  var serviceInterface = {};

  /**
   * @return {object}
   *   A promise.
   */
  serviceInterface.request = function () {
    return $http.get('/endpoint');
  };

  /**
   * @return {object}
   *   A promise.
   */
  serviceInterface.anotherRequest = function () {
    return $http(
      method: 'GET',
      url: '/anotherEndpoint'
    ).then(function (response) {
      // Process a successful response before it is passed on to other code.
      response.decorator = function () {};
      return response;
    });
  };

  return serviceInterface;
}

// Create the example service provider.
example.service('exampleService', ['$http', exampleService]);

The fact that all of the XHR requests from AngularJS are passing through this one service make it a useful place to apply global changes. For example, instead of injecting $http as a dependency, you could instead wrap it in your own service and use that throughout your application:

/**
 * Service definition for a wrapped version of $http.
 */
function wrappedHttp($http) {

  var wrapper = function () {

    // Apply global changes to arguments, or perform other
    // nefarious acts.

    return $http.apply($http, arguments);
  };

  // $http has convenience methods such as $http.get() that we have
  // to pass through as well.
  Object.keys($http).filter(function (key) {
    return (typeof $http[key] === 'function');
  }).forEach(function (key) {
    wrapper[key] = function () {

      // Apply global changes to arguments, or perform other
      // nefarious acts.

       return $http[key].apply($http, arguments);
    };
  });

  return wrapper;
}

// Create the wrapped $http service provider.
example.service('wrappedHttp', ['$http', wrappedHttp]);

// Inject the wrapper instead of $http for a specific service.
// example.service('exampleService', ['$http', exampleService]);
example.service('exampleService', ['wrappedHttp', exampleService]);

It isn't always possible or practical to define and use your own service, however. For example if you are making a late global alteration to a large ongoing project, or want to change the behavior of XHR requests in third party code that can't be altered. In these circumstances you might use $provide.decorator() to globally replace $http with a wrapped version.

var example = angular.module('example');
var provide;

// module.config() allows injection of providers but not instances.
example.config(['$provide', function ($provide) {
  provide = $provide;
}]);

// module.run() allows injection of instances but not providers.
//
// You could use provide.decorator() in module.config(), but then you
// would be stuck if you needed to involve other services, as is often
// the case.
example.run(['someService', 'anotherService', function (someService, anotherService) {
  provide.decorator('$http', function ($delegate) {
    var $http = $delegate;

    var wrapper = function () {

      // Apply global changes to arguments, or perform other
      // nefarious acts.

      return $http.apply($http, arguments);
    };

    // $http has convenience methods such as $http.get() that we have
    // to pass through as well.
    Object.keys($http).filter(function (key) {
      return (typeof $http[key] === 'function');
    }).forEach(function (key) {
      wrapper[key] = function () {

        // Apply global changes to arguments, or perform other
        // nefarious acts.

        return $http[key].apply($http, arguments);
      };
    });

    return wrapper;
  });
}]);

Note that this really does mean global replacement - even for internal AngularJS activities such as loading partials. So you will most likely have to restrict your alterations to specific paths and URLs.