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; + }); });