diff --git a/src/ng/http.js b/src/ng/http.js index deeb6cbb2621..03c720b23c5d 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -162,7 +162,7 @@ function $HttpProvider() { * # General usage * The `$http` service is a function which takes a single argument — a configuration object — * that is used to generate an http request and returns a {@link ng.$q promise} - * with two $http specific methods: `success` and `error`. + * with three $http specific methods: `success`, `error`, and `abort`. * *
      *   $http({method: 'GET', url: '/someUrl'}).
@@ -383,12 +383,13 @@ function $HttpProvider() {
      *      requests with credentials} for more information.
      *
      * @returns {HttpPromise} Returns a {@link ng.$q 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:
+     *   standard `then` method and three http specific methods: `success`, `error`, and `abort`.
+     *   The `then` method takes two arguments a success and an error callback which will be called
+     *   with a response object. The `abort` method will cancel a pending request, causing it to
+     *   fail. 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.
@@ -479,7 +480,7 @@ function $HttpProvider() {
           reqHeaders = extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']},
               defHeaders.common, defHeaders[lowercase(config.method)], config.headers),
           reqData = transformData(config.data, headersGetter(reqHeaders), reqTransformFn),
-          promise;
+          promise, abortFn;
 
       // strip content-type if data is undefined
       if (isUndefined(config.data)) {
@@ -489,13 +490,17 @@ function $HttpProvider() {
       // send request
       promise = sendReq(config, reqData, reqHeaders);
 
+      // save a reference to the abort function
+      abortFn = promise.abort;
 
       // transform future response
       promise = promise.then(transformResponse, transformResponse);
+      promise.abort = abortFn;
 
       // apply interceptors
       forEach(responseInterceptors, function(interceptor) {
         promise = interceptor(promise);
+        promise.abort = abortFn;
       });
 
       promise.success = function(fn) {
@@ -661,6 +666,7 @@ function $HttpProvider() {
     function sendReq(config, reqData, reqHeaders) {
       var deferred = $q.defer(),
           promise = deferred.promise,
+          abortFn,
           cache,
           cachedResp,
           url = buildUrl(config.url, config.params);
@@ -668,6 +674,12 @@ function $HttpProvider() {
       $http.pendingRequests.push(config);
       promise.then(removePendingReq, removePendingReq);
 
+      promise.abort = function() {
+        if (isFunction(abortFn)) {
+          abortFn();
+        }
+      }
+
 
       if (config.cache && config.method == 'GET') {
         cache = isObject(config.cache) ? config.cache : defaultCache;
@@ -696,7 +708,7 @@ function $HttpProvider() {
 
       // if we won't have the response in cache, send the request to the backend
       if (!cachedResp) {
-        $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout,
+        abortFn = $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout,
             config.withCredentials);
       }
 
diff --git a/src/ng/httpBackend.js b/src/ng/httpBackend.js
index 0a12aa23b4a5..eb46601f0a45 100644
--- a/src/ng/httpBackend.js
+++ b/src/ng/httpBackend.js
@@ -52,7 +52,12 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
         delete callbacks[callbackId];
       });
     } else {
-      var xhr = new XHR();
+      var xhr = new XHR(),
+          abortRequest = function() {
+            status = -1;
+            xhr.abort();
+          };
+
       xhr.open(method, url, true);
       forEach(headers, function(value, key) {
         if (value) xhr.setRequestHeader(key, value);
@@ -77,11 +82,10 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
       xhr.send(post || '');
 
       if (timeout > 0) {
-        $browserDefer(function() {
-          status = -1;
-          xhr.abort();
-        }, timeout);
+        $browserDefer(abortRequest, timeout);
       }
+
+      return abortRequest;
     }
 
 
diff --git a/test/ng/httpBackendSpec.js b/test/ng/httpBackendSpec.js
index 06b63c3c8c8d..39d7f4a4fd4f 100644
--- a/test/ng/httpBackendSpec.js
+++ b/test/ng/httpBackendSpec.js
@@ -81,6 +81,27 @@ describe('$httpBackend', function() {
   });
 
 
+  it('should return an abort function', function() {
+    callback.andCallFake(function(status, response) {
+      expect(status).toBe(-1);
+    });
+
+    var abort = $backend('GET', '/url', null, callback);
+    xhr = MockXhr.$$lastInstance;
+    spyOn(xhr, 'abort');
+
+    expect(typeof abort).toBe('function');
+
+    abort();
+    expect(xhr.abort).toHaveBeenCalledOnce();
+
+    xhr.status = 0;
+    xhr.readyState = 4;
+    xhr.onreadystatechange();
+    expect(callback).toHaveBeenCalledOnce();
+  });
+
+
   it('should abort request on timeout', function() {
     callback.andCallFake(function(status, response) {
       expect(status).toBe(-1);
diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js
index bb4de3c1a5e2..0e8dbb1e38d1 100644
--- a/test/ng/httpSpec.js
+++ b/test/ng/httpSpec.js
@@ -978,4 +978,36 @@ describe('$http', function() {
 
     $httpBackend.verifyNoOutstandingExpectation = noop;
   });
+
+
+  it('should abort pending requests', function() {
+    var $httpBackend = jasmine.createSpy('$httpBackend');
+    var abortFn = jasmine.createSpy('abortFn');
+
+    $httpBackend.andCallFake(function(m, u, d, callback) {
+      abortFn.andCallFake(function() {
+        callback(-1, 'bad error', '');
+      });
+      return abortFn;
+    });
+
+    module(function($provide) {
+      $provide.value('$httpBackend', $httpBackend, '');
+    });
+
+    inject(function($http) {
+      $http({method: 'GET', url: 'some.html'}).error(function(data, status, headers, config) {
+        expect(data).toBe('bad error');
+        expect(status).toBe(0);
+        expect(headers()).toEqual({});
+        expect(config.url).toBe('some.html');
+        callback();
+      }).abort();
+      expect($httpBackend).toHaveBeenCalledOnce();
+      expect(abortFn).toHaveBeenCalledOnce();
+      expect(callback).toHaveBeenCalledOnce();
+    });
+
+    $httpBackend.verifyNoOutstandingExpectation = noop;
+  });
 });