Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

feat($http): Added method to abort a pending request. (2nd attempt) #1836

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 44 additions & 11 deletions src/ng/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
*
* <pre>
* $http({method: 'GET', url: '/someUrl'}).
Expand Down Expand Up @@ -390,12 +390,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, and return true or false if the abort succeeded. 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.
Expand Down Expand Up @@ -486,7 +487,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)) {
Expand All @@ -496,13 +497,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) {
Expand Down Expand Up @@ -668,6 +673,9 @@ function $HttpProvider() {
function sendReq(config, reqData, reqHeaders) {
var deferred = $q.defer(),
promise = deferred.promise,
aborted = false,
abortFn,
complete,
cache,
cachedResp,
url = buildUrl(config.url, config.params);
Expand All @@ -694,6 +702,8 @@ function $HttpProvider() {
} else {
resolvePromise(cachedResp, 200, {});
}
promise = promise.then(checkAbortedReq);
abortFn = noop;
}
} else {
// put the promise for the non-transformed response into cache as a placeholder
Expand All @@ -703,18 +713,26 @@ 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);
}

promise.abort = function() {
if (isFunction(abortFn) && !complete) {
aborted = true;
abortFn();
}
return aborted;
}

return promise;


/**
* Callback registered to $httpBackend():
* - caches the response if desired
* - resolves the raw $http promise
* - calls $apply
* - calls $apply if called asynchronously
*/
function done(status, response, headersString) {
if (cache) {
Expand All @@ -727,7 +745,9 @@ function $HttpProvider() {
}

resolvePromise(response, status, headersString);
$rootScope.$apply();
if (!$rootScope.$$phase) {
$rootScope.$apply();
}
}


Expand All @@ -747,7 +767,20 @@ function $HttpProvider() {
}


/**
* Reject a cached response that has been aborted.
*/
function checkAbortedReq(response) {
if (aborted) {
extend(response, {data: null, status: 0});
return $q.reject(response);
}
return response;
}


function removePendingReq() {
complete = true;
var idx = indexOf($http.pendingRequests, config);
if (idx !== -1) $http.pendingRequests.splice(idx, 1);
}
Expand Down
16 changes: 9 additions & 7 deletions src/ng/httpBackend.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,17 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
delete callbacks[callbackId];
});
} else {
var xhr = new XHR();
var status, 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);
});

var status;

// In IE6 and 7, this might be called synchronously when xhr.send below is called and the
// response is in the cache. the promise api will ensure that to the app code the api is
// always async
Expand Down Expand Up @@ -99,11 +102,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;
}


Expand Down
71 changes: 55 additions & 16 deletions src/ngMock/angular-mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -841,8 +841,7 @@ angular.mock.$HttpBackendProvider = function() {
function createHttpBackendMock($delegate, $browser) {
var definitions = [],
expectations = [],
responses = [],
responsesPush = angular.bind(responses, responses.push);
responses = [];

function createResponse(status, data, headers) {
if (angular.isFunction(status)) return status;
Expand All @@ -858,14 +857,40 @@ function createHttpBackendMock($delegate, $browser) {
function $httpBackend(method, url, data, callback, headers) {
var xhr = new MockXhr(),
expectation = expectations[0],
wasExpected = false;
wasExpected = false,
aborted = false,
jsonp = (method.toLowerCase() == 'jsonp');

function prettyPrint(data) {
return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp)
? data
: angular.toJson(data);
}

function createResponse(request) {
return function() {
var response = request.response(method, url, data, headers);
xhr.$$respHeaders = response[2];
callback(response[0], response[1], xhr.getAllResponseHeaders());
}
}

function createAbort(response) {
return function() {
var index = indexOf(responses, response);
if (index >=0) {
responses.splice(index, 1);
abortFn();
return aborted = true;
}
return aborted;
};
}

function abortFn() {
callback(-1, null, null);
}

if (expectation && expectation.match(method, url)) {
if (!expectation.matchData(data))
throw Error('Expected ' + expectation + ' with different data\n' +
Expand All @@ -879,12 +904,9 @@ function createHttpBackendMock($delegate, $browser) {
expectations.shift();

if (expectation.response) {
responses.push(function() {
var response = expectation.response(method, url, data, headers);
xhr.$$respHeaders = response[2];
callback(response[0], response[1], xhr.getAllResponseHeaders());
});
return;
var expectationResponse = createResponse(expectation);
responses.push(expectationResponse);
return jsonp ? undefined : createAbort(expectationResponse);
}
wasExpected = true;
}
Expand All @@ -894,15 +916,23 @@ function createHttpBackendMock($delegate, $browser) {
if (definition.match(method, url, data, headers || {})) {
if (definition.response) {
// if $browser specified, we do auto flush all requests
($browser ? $browser.defer : responsesPush)(function() {
var response = definition.response(method, url, data, headers);
xhr.$$respHeaders = response[2];
callback(response[0], response[1], xhr.getAllResponseHeaders());
});
var definitionResponse = createResponse(definition);
if ($browser) {
var deferId = $browser.defer(definitionResponse);
return jsonp ? undefined : function() {
if($browser.defer.cancel(deferId)) {
abortFn();
return aborted = true;
}
return aborted;
};
} else {
responses.push(definitionResponse);
return jsonp ? undefined : createAbort(definitionResponse);
}
} else if (definition.passThrough) {
$delegate(method, url, data, callback, headers);
return $delegate(method, url, data, callback, headers);
} else throw Error('No response defined !');
return;
}
}
throw wasExpected ?
Expand Down Expand Up @@ -1259,6 +1289,15 @@ function createHttpBackendMock($delegate, $browser) {
}
}

function indexOf(array, obj) {
if (array.indexOf) return array.indexOf(obj);

for ( var i = 0; i < array.length; i++) {
if (obj === array[i]) return i;
}
return -1;
}

function MockHttpExpectation(method, url, data, headers) {

this.data = data;
Expand Down
47 changes: 47 additions & 0 deletions test/ng/httpBackendSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,48 @@ 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 = 200;
xhr.readyState = 4;
xhr.onreadystatechange();
expect(callback).toHaveBeenCalledOnce();
});


it('should not abort a completed request', function() {
callback.andCallFake(function(status, response) {
expect(status).toBe(200);
});

var abort = $backend('GET', '/url', null, callback);
xhr = MockXhr.$$lastInstance;
spyOn(xhr, 'abort');

expect(typeof abort).toBe('function');

xhr.status = 200;
xhr.readyState = 4;
xhr.onreadystatechange();

abort();
expect(xhr.abort).toHaveBeenCalledOnce();
expect(callback).toHaveBeenCalledOnce();
});


it('should abort request on timeout', function() {
callback.andCallFake(function(status, response) {
expect(status).toBe(-1);
Expand Down Expand Up @@ -221,6 +263,11 @@ describe('$httpBackend', function() {
});


it('should respond undefined', function() {
expect($backend('JSONP', '/url')).toBeUndefined();
});


// TODO(vojta): test whether it fires "async-start"
// TODO(vojta): test whether it fires "async-end" on both success and error
});
Expand Down
Loading