AngularĀ $http Service

Matt Zabriskie

@mzabriskie

Source: http://www.wallarc.com/wallpaper/view/704155

History of XHR

In the beginning was the full page reload,

and the full page reload was total crap

Full Page Reload

<!doctype html>
<html>
<head>
    <title>My Site</title>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
</head>
<body>
    <div class="container">
        <div class="header">...</div>
        <div class="content">
            ...
            <form method="POST" action="/path/to/handler.php">
                ...
                <button type="submit">Submit</button>
            </form>
            ...
        </div>
        <div class="footer">...</div>
    </div>
    <script type="text/javascript" src="scripts.js"></script>
</body>
</html>

Full Page Reload

  • Easy to implement
  • Blank screen while loading
  • Poor performance
  • Reload layout, styles, scripts

Fake-jax

<form method="POST" action="/path/to/handler.php"
        target="formHandler" onsubmit="return false;">...</form>

<iframe id="formHandler" onload="handleIframeLoad();"
        style="width: 0; height: 0;"></iframe>

<script type="text/javascript">
    function handleIframeLoad() {
        // Parse response from iframe DOM
    }
</script>

Fake-jax

  • No indication of progress
  • Parsing iFrame DOM is ugly
  • XML is very verbose

XMLHttpRequest

  • Originated within Microsoft for Exchange Server 2000
  • Shipped with Internet Explorer 5.0 in March 1999 (ActiveX)
  • Fully functional XMLHttpRequest landed in Mozilla in 2002
  • Became de facto standard between 2004-2005
  • Internet Explorer 7.0 added XMLHttpRequest object

XMLHttpRequest

var factory = [
  function () {return new XMLHttpRequest()},
  function () {return new ActiveXObject("Msxml2.XMLHTTP")},
  function () {return new ActiveXObject("Msxml3.XMLHTTP")},
  function () {return new ActiveXObject("Microsoft.XMLHTTP")}
];

function getXHR() {
  var xhr = null;
  for (var i=0, l=factory.length; i<l; i++) {
    try { xhr = factory[i](); } catch (e) { continue; }
    break;
  }
  return xhr;
}

var request = getXHR();
request.open('POST', '/path/to/handler', true);
request.setRequestHeader('Content-type','application/x-www-form-urlencoded');
request.onreadystatechange = function () {
  if (request.readyState === 4) {
    if (request.status >= 200 && request.status < 300) {
      alert('success');
    } else {
      alert('error');
    }
  }
};
request.send('foo=bar');

XMLHttpRequest

  • Still no progress indicator
  • Messing with ActiveX variations sucks
  • Does anyone still use XML?

window.fetch

fetch('/users.json')
  .then(function(response) {
    return response.json();
  }).then(function(json) {
    console.log('parsed json', json);
  })
  .catch(function(ex) {
    console.log('parsing failed', ex);
  });

window.fetch

  • Replacement for XHR
  • Currently being developed by Chrome
  • Available as a polyfill

$http

  • $q
  • $httpBackend
  • $cacheFactory

Under the Hood

angular.module('app', [])
.controller('MyCtrl', function($scope, $http) {
  $http({
    url: 'https://api.github.com/users/mzabriskie'
  }).then(function(res) {
    $scope.user = res.data;
  }).catch(function (res) {
    $scope.error = res.status + ' (' + res.statusText + ')';
  });
});

Usage

Config

$http({
  url: '/some/url',
  method: 'get', // default
  params: {foo: 'bar'}, // ?foo=bar
  data: {name: 'Fred'}, // POST body
  headers: {'X-Requested-With': '$http'},
  xsrfHeaderName: 'X-XSRF-TOKEN', // default
  xsrfCookieName: 'XSRF-TOKEN', // default
  transformRequest: function (data, headers) {}, // default JSON serializer
  transformResponse: function (data, headers) {}, // default JSON deserializer
  cache: false, // default
  timeout: 1000,
  withCredentials: false, // default
  responseType: 'json'
});

Response

$http({
  url: '/some/url'
})
.then(function (res) {
  // Data that came back as the response body
  console.log(res.data);

  // Response headers (object)
  console.log(res.headers);

  // Response status (200)
  console.log(res.status);

  // Config that was used for the request
  console.log(res.config);
});

Shortcuts

// $http.get(url[, config])
$http.get('/some/url').then(function (res) {});

// $http.post(url, data[, config])
$http.post('/some/url', {
  firstName: 'Fred',
  lastName: 'Flintstone'
})
.then(function (res) {});

Defaults

angular.module('app', [])
.config(function ($httpProvider) {
  // Set default xsrfHeaderName for all requests
  $httpProvider.defaults.xsrfHeaderName = 'MY-XSRF-TOKEN';

  // Add custom request header to all requests
  $httpProvider.defaults.headers.common['My-Custom-Header'] = 'something amazing';
});

Transformers

Transformers

  • Transform both requests & responses
  • Transform data sent/received
  • Transform headers

Built-in Transformers

  • Transform data of requests to JSON
  • Transform data of responses from JSON
  • Protection against JSON vulnerabilities

Custom Transformers

angular.module('app', [])
.config(function ($httpProvider) {
  // Transform response data to convert ISO-8601 date format to Date object
  var ISO_8601 = /(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})Z/;
  $httpProvider.defaults.transformResponse.push(function (data, headers) {
    for (var k in data) {
      if (data.hasOwnProperty(k)) {
        if (ISO_8601.test(data[k])) {
          data[k] = new Date(Date.parse(data[k]));
        }
      }
    }
    return data;
  });
})

Gotcha

angular.module('app', [])
.config(function ($httpProvider) {
  $httpProvider.defaults.transformRequest = function (data, headers) {
    if (typeof data.firstName !== 'undefined') {
      data.firstName = 'Joe';
    }
    return data;
  };
})
.controller('MyCtrl', function($scope, $http) {
  $scope.user = {};
  
  $scope.addUser = function (user) {
    $http.post('/users', angular.toJson(user))
    .then(function(res) {
      $scope.response = res;
      $scope.user = {};
    });
  };
});

Workaround

function appendTransform(defaults, transform) {
  // We can't guarantee that the default transformation is an array
  defaults = angular.isArray(defaults) ? defaults : [defaults];

  // Append the new transformation to the defaults
  return defaults.concat(transform);
}

$http({
  url: '/some/url',
  method: 'GET',
  transformResponse: appendTransform(
    $http.defaults.transformResponse,
    function(value) {
      return doTransform(value);
    }
  )
});

Interceptors

Interceptors

  • Intercept request before it's dispatched
  • Intercept response after it returns
  • Intercept request/response errors
  • Modify the config for the request
  • Modify the response object

Simple Interceptor

angular.module('app', [])
.config(function ($httpProvider) {
  $httpProvider.interceptors.push('loggingInterceptor');
})
.factory('loggingInterceptor', function ($log) {
  return {
    request: function (config) {
      $log.info(config.method + ' ' + config.url);
      return config;
    }
  };
})

Log Request Time

angular.module('app', [])
.config(function ($httpProvider) {
  $httpProvider.interceptors.push('timestampInterceptor');
})
.factory('timestampInterceptor', function () {
  return {
    request: function (config) {
      config.requestTime = Date.now();
      return config;
    },
    response: function (response) {
      response.config.responseTime = Date.now();
      return response;
    }
  }
})
.controller('MyCtrl', function($http, $log) {
    $http.get('https://api.github.com/users/mzabriskie/repos'),
      .then(function (res) {
        var time = res.config.responseTime - res.config.requestTime;
        $log.info('Request took ' + (time / 1000) + ' seconds');
      });
});

Recover Session

module.config(function($httpProvider) {
    $httpProvider.interceptors.push('sessionRecoverer');
});
module.factory('sessionRecoverer', function($q, $injector) {
  return {
    responseError: function(response) {
      // Session has expired
      if (response.status === 419){
        var SessionService = $injector.get('SessionService');
        var $http = $injector.get('$http');
        var deferred = $q.defer();

        // Create a new session (recover the session)
        SessionService.login().then(deferred.resolve, deferred.reject);

        // When the session recovered, make the same backend call again
        return deferred.promise.then(function() {
          return $http(response.config);
        });
      }
      return $q.reject(response);
    }
  };
});

Caching

Caching

  • Allows caching response data
  • Only works with GET method
  • Supports custom cache

Simple Cache

angular.module('app', [])
.controller('MyCtrl', function ($scope, $http) {
  $http.get('/some/url', { cache: true });
});

Custom Cache

angular.module('app', [])
.factory('customCache', function ($cacheFactory) {
  return $cacheFactory('custom');
})
.controller('MyCtrl', function ($scope, $http, customCache) {
  $scope.loadData = function () {
    $http.get('/some/url', { cache: customCache })
      .then(function (res) {
        $scope.data = res.data;
      });
  };

  $scope.clearData = function () {
    customCache.remove('/some/url');
  };
});

Concurrency

$q.all

angular.module('app', [])
.controller('MyCtrl', function($scope, $http, $q) {
  $q.all([
    $http.get('/user/12345'),
    $http.get('/user/12345/permissions')
    ]).then(function (res) {
      $scope.user = res[0].data;
      $scope.perm = res[1].data;
    });
});

Resources

Thank You!

@mzabriskie

Angular $http Service

By Matt Zabriskie

Angular $http Service

My talk for AngularJS Utah October 2014

  • 3,241