diff options
| author | Igor Minar | 2011-12-28 09:26:22 -0800 | 
|---|---|---|
| committer | Vojta Jina | 2012-01-09 13:17:48 -0800 | 
| commit | a13b5ed3bc337a493029815c595b89c39eb95af6 (patch) | |
| tree | 2ca5380d5cf5aea68218280cccda5d0221517454 /src/service | |
| parent | 63cca9afbcf7a772086eb4582d2f409c39e0ed12 (diff) | |
| download | angular.js-a13b5ed3bc337a493029815c595b89c39eb95af6.tar.bz2 | |
fix($http): fix and cleanup $http and friends
$http:
- use promises internally
- get rid of XhrFuture that was previously used internally
- get rid of $browser.defer calls for async stuff (serving from cache),
  promises will take care of asynchronicity
- fix transformation bugs (when caching requested + multiple request
  pending + error is returned)
- get rid of native header parsing and instead just lazily parse the
  header string
$httpBackend:
- don't return raw/mock XMLHttpRequest object (we don't use it for
  anything anymore)
- call the callback with response headers string
mock $httpBackend:
- unify response api for expect and when
- call the callback with response headers string
- changed the expect/when failure error message so that EXPECTED and GOT
  values are aligned
Conflicts:
	src/service/http.js
	test/service/compilerSpec.js
	test/service/httpSpec.js
Diffstat (limited to 'src/service')
| -rw-r--r-- | src/service/http.js | 680 | ||||
| -rw-r--r-- | src/service/httpBackend.js | 40 | 
2 files changed, 326 insertions, 394 deletions
diff --git a/src/service/http.js b/src/service/http.js index e6a42b65..bd8e6e65 100644 --- a/src/service/http.js +++ b/src/service/http.js @@ -4,11 +4,13 @@   * Parse headers into key value object   *   * @param {string} headers Raw headers as a string - * @returns {Object} Parsed headers as key valu object + * @returns {Object} Parsed headers as key value object   */  function parseHeaders(headers) {    var parsed = {}, key, val, i; +  if (!headers) return parsed; +    forEach(headers.split('\n'), function(line) {      i = line.indexOf(':');      key = lowercase(trim(line.substr(0, i))); @@ -26,6 +28,34 @@ function parseHeaders(headers) {    return parsed;  } + +/** + * Returns a function that provides access to parsed headers. + * + * Headers are lazy parsed when first requested. + * @see parseHeaders + * + * @param {(string|Object)} headers Headers to provide access to. + * @returns {function(string=)} Returns a getter function which if called with: + * + *   - if called with single an argument returns a single header value or null + *   - if called with no arguments returns an object containing all headers. + */ +function headersGetter(headersString) { +  var headers = isObject(headersString) ? headersString : undefined; + +  return function(name) { +    if (!headers) headers =  parseHeaders(headersString); + +    if (name) { +      return headers[lowercase(name)] || null; +    } + +    return headers; +  }; +} + +  /**   * Chain all given functions   * @@ -36,7 +66,7 @@ function parseHeaders(headers) {   * @param {*=} param Optional parameter to be passed to all transform functions.   * @returns {*} Transformed data.   */ -function transform(data, fns, param) { +function transformData(data, fns, param) {    if (isFunction(fns))      return fns(data); @@ -48,13 +78,18 @@ function transform(data, fns, param) {  } +function isSuccess(status) { +  return 200 <= status && status < 300; +} + +  function $HttpProvider() {    var JSON_START = /^\s*(\[|\{[^\{])/,        JSON_END = /[\}\]]\s*$/,        PROTECTION_PREFIX = /^\)\]\}',?\n/;    var $config = this.defaults = { -    // transform in-coming reponse data +    // transform incoming response data      transformResponse: function(data) {        if (isString(data)) {          // strip json vulnerability protection prefix @@ -65,7 +100,7 @@ function $HttpProvider() {        return data;      }, -    // transform out-going request data +    // transform outgoing request data      transformRequest: function(d) {        return isObject(d) ? toJson(d) : d;      }, @@ -81,431 +116,328 @@ function $HttpProvider() {      }    }; -  var responseInterceptors = this.responseInterceptors = []; +  var providerResponseInterceptors = this.responseInterceptors = [];    this.$get = ['$httpBackend', '$browser', '$exceptionHandler', '$cacheFactory', '$rootScope', '$q', '$injector',        function($httpBackend, $browser, $exceptionHandler, $cacheFactory, $rootScope, $q, $injector) { -  var defaultCache = $cacheFactory('$http'); +    var defaultCache = $cacheFactory('$http'), +        responseInterceptors = []; -  forEach(responseInterceptors, function(interceptor, index) { -    if (isString(interceptor)) { -      responseInterceptors[index] = $injector.get(interceptor); -    } -  }); +    forEach(providerResponseInterceptors, function(interceptor) { +      responseInterceptors.push(isString(interceptor) ? $injector.get(interceptor) : interceptor); +    }); -  /** -   * @ngdoc function -   * @name angular.module.ng.$http -   * @requires $httpBacked -   * @requires $browser -   * @requires $exceptionHandler -   * @requires $cacheFactory -   * -   * @param {object} config Object describing the request to be made and how it should be processed. -   *    The object has following properties: -   * -   *    - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) -   *    - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. -   *    - **data** – `{string|Object}` – Data to be sent as the request message data. -   *    - **headers** – `{Object}` – Map of strings representing HTTP headers to send to the server. -   *    - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the -   *      GET request, otherwise if a cache instance built with $cacheFactory, this cache will be -   *      used for caching. -   * -   * @returns {HttpPromise} Returns a promise object with the standard `then` method and two http -   *   specific methods: `success` and `error`. The `then` method takes two arguments a success and -   *   an error callback which will be called with a response object. The `success` and `error` -   *   methods take a single argument - a function that will be called when the request succeeds or -   *   fails respectively. The arguments passed into these functions are destructured representation -   *   of the response object passed into the `then` method. The response object has these -   *   properties: -   * -   *   - **data** – `{string|Object}` – The response body transformed with the transform functions. -   *   - **status** – `{number}` – HTTP status code of the response. -   *   - **headers** – `{function([headerName])}` – Header getter function. -   *   - **config** – `{Object}` – The configuration object that was used to generate the request. -   * -   * @property {Array.<Object>} pendingRequests Array of config objects for pending requests. -   *   This is primarily meant to be used for debugging purposes. -   * -   * @description -   * $http is a service through which XHR and JSONP requests can be made. -   */ -  function $http(config) { -    var req = new XhrFuture().send(config), -        deferredResp = $q.defer(), -        promise = deferredResp.promise; - -    forEach(responseInterceptors, function(interceptor) { -      promise = interceptor(promise); -    }); +    /** +     * @ngdoc function +     * @name angular.module.ng.$http +     * @requires $httpBacked +     * @requires $browser +     * @requires $exceptionHandler //TODO(i): still needed? +     * @requires $cacheFactory +     * +     * @param {object} config Object describing the request to be made and how it should be processed. +     *    The object has following properties: +     * +     *    - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) +     *    - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. +     *    - **data** – `{string|Object}` – Data to be sent as the request message data. +     *    - **headers** – `{Object}` – Map of strings representing HTTP headers to send to the server. +     *    - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the +     *      GET request, otherwise if a cache instance built with $cacheFactory, this cache will be +     *      used for caching. +     * +     * @returns {HttpPromise} Returns a promise object with the standard `then` method and two http +     *   specific methods: `success` and `error`. The `then` method takes two arguments a success and +     *   an error callback which will be called with a response object. The `success` and `error` +     *   methods take a single argument - a function that will be called when the request succeeds or +     *   fails respectively. The arguments passed into these functions are destructured representation +     *   of the response object passed into the `then` method. The response object has these +     *   properties: +     * +     *   - **data** – `{string|Object}` – The response body transformed with the transform functions. +     *   - **status** – `{number}` – HTTP status code of the response. +     *   - **headers** – `{function([headerName])}` – Header getter function. +     *   - **config** – `{Object}` – The configuration object that was used to generate the request. +     * +     * @property {Array.<Object>} pendingRequests Array of config objects for pending requests. +     *   This is primarily meant to be used for debugging purposes. +     * +     * @description +     * $http is a service through which XHR and JSONP requests can be made. +     */ +    function $http(config) { +      config.method = uppercase(config.method); -    promise.success = function(fn) { -      promise.then(function(response) { -        fn(response.data, response.status, response.headers, config); -      }); -      return promise; -    }; +      var reqTransformFn = config.transformRequest || $config.transformRequest, +          respTransformFn = config.transformResponse || $config.transformResponse, +          reqData = transformData(config.data, reqTransformFn), +          defHeaders = $config.headers, +          reqHeaders = extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']}, +              defHeaders.common, defHeaders[lowercase(config.method)], config.headers), +          promise; -    promise.error = function(fn) { -      promise.then(null, function(response) { -        fn(response.data, response.status, response.headers, config); -      }); -      return promise; -    }; -    req.on('success', function(data, status, headers) { -      deferredResp.resolve({data: data, status: status, headers: headers, config: config}); -    }).on('error', function(data, status, headers) { -      deferredResp.reject({data: data, status: status, headers: headers, config: config}); -    }); +      // send request +      promise = sendReq(config, reqData, reqHeaders); + -    return promise; -  } - -  $http.pendingRequests = []; - -  /** -   * @ngdoc method -   * @name angular.module.ng.$http#get -   * @methodOf angular.module.ng.$http -   * -   * @description -   * Shortcut method to perform `GET` request -   * -   * @param {string} url Relative or absolute URL specifying the destination of the request -   * @param {Object=} config Optional configuration object -   * @returns {HttpPromise} Future object -   */ - -  /** -   * @ngdoc method -   * @name angular.module.ng.$http#delete -   * @methodOf angular.module.ng.$http -   * -   * @description -   * Shortcut method to perform `DELETE` request -   * -   * @param {string} url Relative or absolute URL specifying the destination of the request -   * @param {Object=} config Optional configuration object -   * @returns {HttpPromise} Future object -   */ - -  /** -   * @ngdoc method -   * @name angular.module.ng.$http#head -   * @methodOf angular.module.ng.$http -   * -   * @description -   * Shortcut method to perform `HEAD` request -   * -   * @param {string} url Relative or absolute URL specifying the destination of the request -   * @param {Object=} config Optional configuration object -   * @returns {XhrFuture} Future object -   */ - -  /** -   * @ngdoc method -   * @name angular.module.ng.$http#patch -   * @methodOf angular.module.ng.$http -   * -   * @description -   * Shortcut method to perform `PATCH` request -   * -   * @param {string} url Relative or absolute URL specifying the destination of the request -   * @param {Object=} config Optional configuration object -   * @returns {HttpPromise} Future object -   */ - -  /** -   * @ngdoc method -   * @name angular.module.ng.$http#jsonp -   * @methodOf angular.module.ng.$http -   * -   * @description -   * Shortcut method to perform `JSONP` request -   * -   * @param {string} url Relative or absolute URL specifying the destination of the request. -   *                     Should contain `JSON_CALLBACK` string. -   * @param {Object=} config Optional configuration object -   * @returns {XhrFuture} Future object -   */ -  createShortMethods('get', 'delete', 'head', 'patch', 'jsonp'); - -  /** -   * @ngdoc method -   * @name angular.module.ng.$http#post -   * @methodOf angular.module.ng.$http -   * -   * @description -   * Shortcut method to perform `POST` request -   * -   * @param {string} url Relative or absolute URL specifying the destination of the request -   * @param {*} data Request content -   * @param {Object=} config Optional configuration object -   * @returns {HttpPromise} Future object -   */ - -  /** -   * @ngdoc method -   * @name angular.module.ng.$http#put -   * @methodOf angular.module.ng.$http -   * -   * @description -   * Shortcut method to perform `PUT` request -   * -   * @param {string} url Relative or absolute URL specifying the destination of the request -   * @param {*} data Request content -   * @param {Object=} config Optional configuration object -   * @returns {XhrFuture} Future object -   */ -  createShortMethodsWithData('post', 'put'); - -  return $http; - -  function createShortMethods(names) { -    forEach(arguments, function(name) { -      $http[name] = function(url, config) { -        return $http(extend(config || {}, { -          method: name, -          url: url -        })); +      // transform future response +      promise = promise.then(transformResponse, transformResponse); + +      // apply interceptors +      forEach(responseInterceptors, function(interceptor) { +        promise = interceptor(promise); +      }); + +      promise.success = function(fn) { +        promise.then(function(response) { +          fn(response.data, response.status, response.headers, config); +        }); +        return promise;        }; -    }); -  } - -  function createShortMethodsWithData(name) { -    forEach(arguments, function(name) { -      $http[name] = function(url, data, config) { -        return $http(extend(config || {}, { -          method: name, -          url: url, -          data: data -        })); + +      promise.error = function(fn) { +        promise.then(null, function(response) { +          fn(response.data, response.status, response.headers, config); +        }); +        return promise;        }; -    }); -  } - -  /** -   * Represents Request object, returned by $http() -   * -   * !!! ACCESSES CLOSURE VARS: -   * $httpBackend, $browser, $config, $log, $rootScope, defaultCache, $http.pendingRequests -   */ -  function XhrFuture() { -    var rawRequest, parsedHeaders, -        cfg = {}, callbacks = [], -        defHeaders = $config.headers, -        self = this; + +      return promise; + +      function transformResponse(response) { +        // make a copy since the response must be cacheable +        var resp = extend({}, response, { +          data: transformData(response.data, respTransformFn, response.headers) +        }); +        return (isSuccess(response.status)) +          ? resp +          : $q.reject(resp); +      } +    } + +    $http.pendingRequests = [];      /** -     * Callback registered to $httpBackend(): -     *  - caches the response if desired -     *  - calls fireCallbacks() -     *  - clears the reference to raw request object +     * @ngdoc method +     * @name angular.module.ng.$http#get +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `GET` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {Object=} config Optional configuration object +     * @returns {HttpPromise} Future object       */ -    function done(status, response) { -      // aborted request or jsonp -      if (!rawRequest) parsedHeaders = {}; - -      if (cfg.cache && cfg.method == 'GET') { -        var cache = isObject(cfg.cache) && cfg.cache || defaultCache; -        if (200 <= status && status < 300) { -          parsedHeaders = parsedHeaders || parseHeaders(rawRequest.getAllResponseHeaders()); -          cache.put(cfg.url, [status, response, parsedHeaders]); -        } else { -          // remove future object from cache -          cache.remove(cfg.url); -        } -      } -      fireCallbacks(response, status); -      // TODO(i): we can't null the rawRequest because we might need to be able to call -      // rawRequest.getAllResponseHeaders from a promise -      // rawRequest = null; -    } +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#delete +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `DELETE` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {Object=} config Optional configuration object +     * @returns {HttpPromise} Future object +     */      /** -     * Fire all registered callbacks for given status code +     * @ngdoc method +     * @name angular.module.ng.$http#head +     * @methodOf angular.module.ng.$http       * -     * This method when: -     *  - serving response from real request -     *  - serving response from cache +     * @description +     * Shortcut method to perform `HEAD` request       * -     * It does: -     *  - transform the response -     *  - call proper callbacks -     *  - log errors -     *  - apply the $scope -     *  - clear parsed headers +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {Object=} config Optional configuration object +     * @returns {XhrFuture} Future object       */ -    function fireCallbacks(response, status) { -      var strStatus = status + ''; - -      // transform the response -      response = transform(response, cfg.transformResponse || $config.transformResponse, rawRequest); - -      var idx; // remove from pending requests -      if ((idx = indexOf($http.pendingRequests, cfg)) !== -1) -        $http.pendingRequests.splice(idx, 1); - -      // normalize internal statuses to 0 -      status = Math.max(status, 0); -      forEach(callbacks, function(callback) { -        if (callback.regexp.test(strStatus)) { -          try { -            // use local var to call it without context -            var fn = callback.fn; -            fn(response, status, headers); -          } catch(e) { -            $exceptionHandler(e); -          } -        } -      }); -      $rootScope.$apply(); -      parsedHeaders = null; -    } +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#patch +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `PATCH` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {Object=} config Optional configuration object +     * @returns {HttpPromise} Future object +     */      /** -     * This is the third argument in any user callback -     * @see parseHeaders +     * @ngdoc method +     * @name angular.module.ng.$http#jsonp +     * @methodOf angular.module.ng.$http       * -     * Return single header value or all headers parsed as object. -     * Headers all lazy parsed when first requested. +     * @description +     * Shortcut method to perform `JSONP` request       * -     * @param {string=} name Name of header -     * @returns {string|Object} +     * @param {string} url Relative or absolute URL specifying the destination of the request. +     *                     Should contain `JSON_CALLBACK` string. +     * @param {Object=} config Optional configuration object +     * @returns {XhrFuture} Future object       */ -    function headers(name) { -      if (name) { -        return parsedHeaders ? -          parsedHeaders[lowercase(name)] || null : -          rawRequest.getResponseHeader(name); -      } +    createShortMethods('get', 'delete', 'head', 'patch', 'jsonp'); -      parsedHeaders = parsedHeaders || parseHeaders(rawRequest.getAllResponseHeaders()); +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#post +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `POST` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {*} data Request content +     * @param {Object=} config Optional configuration object +     * @returns {HttpPromise} Future object +     */ -      return parsedHeaders; +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#put +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `PUT` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {*} data Request content +     * @param {Object=} config Optional configuration object +     * @returns {XhrFuture} Future object +     */ +    createShortMethodsWithData('post', 'put'); + + +    return $http; + + +    function createShortMethods(names) { +      forEach(arguments, function(name) { +        $http[name] = function(url, config) { +          return $http(extend(config || {}, { +            method: name, +            url: url +          })); +        }; +      });      } + +    function createShortMethodsWithData(name) { +      forEach(arguments, function(name) { +        $http[name] = function(url, data, config) { +          return $http(extend(config || {}, { +            method: name, +            url: url, +            data: data +          })); +        }; +      }); +    } + +      /** -     * Retry the request +     * Makes the request       * -     * @param {Object=} config Optional config object to extend the original configuration -     * @returns {HttpPromise} +     * !!! ACCESSES CLOSURE VARS: +     * $httpBackend, $config, $log, $rootScope, defaultCache, $http.pendingRequests       */ -    this.retry = function(config) { -      if (rawRequest) throw 'Can not retry request. Abort pending request first.'; +    function sendReq(config, reqData, reqHeaders) { +      var deferred = $q.defer(), +          promise = deferred.promise, +          cache, +          cachedResp; -      extend(cfg, config); -      cfg.method = uppercase(cfg.method); +      $http.pendingRequests.push(config); +      promise.then(removePendingReq, removePendingReq); -      var data = transform(cfg.data, cfg.transformRequest || $config.transformRequest), -          headers = extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']}, -                           defHeaders.common, defHeaders[lowercase(cfg.method)], cfg.headers); -      var cache = isObject(cfg.cache) && cfg.cache || defaultCache, -          fromCache; +      if (config.cache && config.method == 'GET') { +        cache = isObject(config.cache) ? config.cache : defaultCache; +      } -      if (cfg.cache && cfg.method == 'GET') { -        fromCache = cache.get(cfg.url); -        if (fromCache) { -          if (fromCache instanceof XhrFuture) { -            // cached request has already been sent, but there is no reponse yet, +      if (cache) { +        cachedResp = cache.get(config.url); +        if (cachedResp) { +          if (cachedResp.then) { +            // cached request has already been sent, but there is no response yet,              // we need to register callback and fire callbacks when the request is back              // note, we have to get the values from cache and perform transformations on them,              // as the configurations don't have to be same -            fromCache.on('always', function() { -              var requestFromCache = cache.get(cfg.url); -              parsedHeaders = requestFromCache[2]; -              fireCallbacks(requestFromCache[1], requestFromCache[0]); -            }); +            cachedResp.then(removePendingReq, removePendingReq); +            return cachedResp;            } else { -            // serving from cache - still needs to be async -            $browser.defer(function() { -              parsedHeaders = fromCache[2]; -              fireCallbacks(fromCache[1], fromCache[0]); -            }); +            // serving from cache +            if (isArray(cachedResp)) { +              resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2])); +            } else { +              resolvePromise(cachedResp, 200, {}); +            }            }          } else { -          // put future object into cache -          cache.put(cfg.url, self); +          // put the promise for the non-transformed response into cache as a placeholder +          cache.put(config.url, promise);          }        } -      // really send the request -      if (!cfg.cache || cfg.method !== 'GET' || !fromCache) { -        rawRequest = $httpBackend(cfg.method, cfg.url, data, done, headers, cfg.timeout); +      // if we won't have the response in cache, send the request to the backend +      if (!cachedResp) { +        $httpBackend(config.method, config.url, reqData, done, reqHeaders, config.timeout);        } -      $http.pendingRequests.push(cfg); -      return self; -    }; +      return promise; -    // just alias so that in stack trace we can see send() instead of retry() -    this.send = this.retry; -    /** -     * Abort the request -     */ -    this.abort = function() { -      if (rawRequest) { -        rawRequest.abort(); +      /** +       * Callback registered to $httpBackend(): +       *  - caches the response if desired +       *  - resolves the raw $http promise +       *  - calls $apply +       */ +      function done(status, response, headersString) { +        if (cache) { +          if (isSuccess(status)) { +            cache.put(config.url, [status, response, parseHeaders(headersString)]); +          } else { +            // remove promise from the cache +            cache.remove(config.url); +          } +        } + +        resolvePromise(response, status, headersString); +        $rootScope.$apply();        } -      return this; -    }; -    /** -     * Register a callback function based on status code -     * Note: all matched callbacks will be called, preserving registered order ! -     * -     * Internal statuses: -     *  `-2` = jsonp error -     *  `-1` = timeout -     *   `0` = aborted -     * -     * @example -     *   .on('2xx', function(){}); -     *   .on('2x1', function(){}); -     *   .on('404', function(){}); -     *   .on('20x,3xx', function(){}); -     *   .on('success', function(){}); -     *   .on('error', function(){}); -     *   .on('always', function(){}); -     *   .on('timeout', function(){}); -     *   .on('abort', function(){}); -     * -     * @param {string} pattern Status code pattern with "x" for any number -     * @param {function(*, number, function)} callback Function to be called when response arrives -     * @returns {XhrFuture} -     */ -    this.on = function(pattern, callback) { -      var alias = { -        success: '2xx', -        error: '-2,-1,0,4xx,5xx', -        always: 'xxx,xx,x', -        timeout: '-1', -        abort: '0' -      }; -      callbacks.push({ -        fn: callback, -        // create regexp from given pattern -        regexp: new RegExp('^(' + (alias[pattern] || pattern).replace(/,/g, '|'). -                                                              replace(/x/g, '.') + ')$') -      }); +      /** +       * Resolves the raw $http promise. +       */ +      function resolvePromise(response, status, headers) { +        // normalize internal statuses to 0 +        status = Math.max(status, 0); -      return this; -    }; +        (isSuccess(status) ? deferred.resolve : deferred.reject)({ +          data: response, +          status: status, +          headers: headersGetter(headers), +          config: config +        }); +      } -    /** -     * Configuration object of the request -     */ -    this.config = cfg; -  } -}]; -} +      function removePendingReq() { +        var idx = indexOf($http.pendingRequests, config); +        if (idx !== -1) $http.pendingRequests.splice(idx, 1); +      } +    } +  }]; +} diff --git a/src/service/httpBackend.js b/src/service/httpBackend.js index c64519d9..c3114814 100644 --- a/src/service/httpBackend.js +++ b/src/service/httpBackend.js @@ -14,6 +14,11 @@ var XHR = window.XMLHttpRequest || function() {   * @requires $document   *   * @description + * HTTP backend used by the {@link angular.module.ng.$http service} that delegates to + * XMLHttpRequest object. + * + * During testing this implementation is swapped with {@link angular.module.ngMock.$httpBackend mock + * $httpBackend} which can be trained with responses.   */  function $HttpBackendProvider() {    this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) { @@ -46,24 +51,21 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, body, locati        var xhr = new XHR();        xhr.open(method, url, true);        forEach(headers, function(value, key) { -          if (value) xhr.setRequestHeader(key, value); +        if (value) xhr.setRequestHeader(key, value);        });        var status; -      xhr.send(post || ''); -      // IE6, IE7 bug - does sync when serving from cache -      if (xhr.readyState == 4) { -        $browserDefer(function() { -          completeRequest(callback, status || xhr.status, xhr.responseText); -        }, 0); -      } else { -        xhr.onreadystatechange = function() { -          if (xhr.readyState == 4) { -            completeRequest(callback, status || xhr.status, xhr.responseText); -          } -        }; -      } +      // In IE6 and 7, this might be called synchronously when xhr.send below is called and the +      // response is in the cache +      xhr.onreadystatechange = function() { +        if (xhr.readyState == 4) { +          completeRequest( +              callback, status || xhr.status, xhr.responseText, xhr.getAllResponseHeaders()); +        } +      }; + +      xhr.send(post || '');        if (timeout > 0) {          $browserDefer(function() { @@ -71,23 +73,21 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, body, locati            xhr.abort();          }, timeout);        } - -      return xhr;      } -    function completeRequest(callback, status, response) { + +    function completeRequest(callback, status, response, headersString) {        // URL_MATCH is defined in src/service/location.js        var protocol = (url.match(URL_MATCH) || ['', locationProtocol])[1];        // fix status code for file protocol (it's always 0) -      status = protocol == 'file' ? (response ? 200 : 404) : status; +      status = (protocol == 'file') ? (response ? 200 : 404) : status;        // normalize IE bug (http://bugs.jquery.com/ticket/1450)        status = status == 1223 ? 204 : status; -      callback(status, response); +      callback(status, response, headersString);        $browser.$$completeOutstandingRequest(noop);      }    };  } -  | 
