From fb16ce93d83924267953eaa7560119e02f746adc Mon Sep 17 00:00:00 2001 From: David Bennett Date: Wed, 28 Nov 2012 18:50:27 -0600 Subject: [PATCH 1/3] Added new feature to abort a pending request. Fixes issue #1159. --- src/ng/http.js | 15 +++++++++++++-- src/ng/httpBackend.js | 12 ++++++++---- test/ng/httpBackendSpec.js | 21 +++++++++++++++++++++ test/ng/httpSpec.js | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/ng/http.js b/src/ng/http.js index deeb6cbb2621..6eae0f3de3e2 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -479,7 +479,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 +489,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 +665,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 +673,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 +707,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..856c0c9988c0 100644 --- a/src/ng/httpBackend.js +++ b/src/ng/httpBackend.js @@ -77,11 +77,15 @@ 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; + + function abortRequest() { + status = -1; + xhr.abort(); + }; } 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; + }); }); From 2ce20700980017aaf834494c3fbd366977251947 Mon Sep 17 00:00:00 2001 From: David Bennett Date: Wed, 28 Nov 2012 19:08:40 -0600 Subject: [PATCH 2/3] Update docs. --- src/ng/http.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/ng/http.js b/src/ng/http.js index 6eae0f3de3e2..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.

From 1ba40211671aa22574fa4c53648588d1fcaea195 Mon Sep 17 00:00:00 2001
From: David Bennett 
Date: Wed, 28 Nov 2012 19:29:05 -0600
Subject: [PATCH 3/3] Fix an issue with Firefox.

---
 src/ng/httpBackend.js | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/ng/httpBackend.js b/src/ng/httpBackend.js
index 856c0c9988c0..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);
@@ -81,11 +86,6 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
       }
 
       return abortRequest;
-
-      function abortRequest() {
-        status = -1;
-        xhr.abort();
-      };
     }