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

Commit 9f4f593

Browse files
dbinitIgorMinar
authored andcommitted
feat($http): add support for aborting via timeout promises
If the timeout argument is a promise, abort the request when it is resolved. Implemented by adding support to $httpBackend service and $httpBackend mock service. This api can also be used to explicitly abort requests while keeping the communication between the deffered and promise unidirectional. Closes #1159
1 parent 27a8824 commit 9f4f593

File tree

7 files changed

+127
-23
lines changed

7 files changed

+127
-23
lines changed

src/ng/http.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,8 @@ function $HttpProvider() {
548548
* GET request, otherwise if a cache instance built with
549549
* {@link ng.$cacheFactory $cacheFactory}, this cache will be used for
550550
* caching.
551-
* - **timeout** – `{number}` – timeout in milliseconds.
551+
* - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise}
552+
* that should abort the request when resolved.
552553
* - **withCredentials** - `{boolean}` - whether to to set the `withCredentials` flag on the
553554
* XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5
554555
* requests with credentials} for more information.
@@ -927,7 +928,7 @@ function $HttpProvider() {
927928
}
928929

929930
resolvePromise(response, status, headersString);
930-
$rootScope.$apply();
931+
if (!$rootScope.$$phase) $rootScope.$apply();
931932
}
932933

933934

src/ng/httpBackend.js

+11-6
Original file line numberDiff line numberDiff line change
@@ -107,20 +107,25 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
107107
}
108108

109109
if (timeout > 0) {
110-
var timeoutId = $browserDefer(function() {
111-
status = -1;
112-
jsonpDone && jsonpDone();
113-
xhr && xhr.abort();
114-
}, timeout);
110+
var timeoutId = $browserDefer(timeoutRequest, timeout);
111+
} else if (timeout && timeout.then) {
112+
timeout.then(timeoutRequest);
115113
}
116114

117115

116+
function timeoutRequest() {
117+
status = -1;
118+
jsonpDone && jsonpDone();
119+
xhr && xhr.abort();
120+
}
121+
118122
function completeRequest(callback, status, response, headersString) {
119123
// URL_MATCH is defined in src/service/location.js
120124
var protocol = (url.match(SERVER_MATCH) || ['', locationProtocol])[1];
121125

122-
// cancel timeout
126+
// cancel timeout and subsequent timeout promise resolution
123127
timeoutId && $browserDefer.cancel(timeoutId);
128+
jsonpDone = xhr = null;
124129

125130
// fix status code for file protocol (it's always 0)
126131
status = (protocol == 'file') ? (response ? 200 : 404) : status;

src/ngMock/angular-mocks.js

+26-12
Original file line numberDiff line numberDiff line change
@@ -937,7 +937,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
937937
}
938938

939939
// TODO(vojta): change params to: method, url, data, headers, callback
940-
function $httpBackend(method, url, data, callback, headers) {
940+
function $httpBackend(method, url, data, callback, headers, timeout) {
941941
var xhr = new MockXhr(),
942942
expectation = expectations[0],
943943
wasExpected = false;
@@ -948,6 +948,28 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
948948
: angular.toJson(data);
949949
}
950950

951+
function wrapResponse(wrapped) {
952+
if (!$browser && timeout && timeout.then) timeout.then(handleTimeout);
953+
954+
return handleResponse;
955+
956+
function handleResponse() {
957+
var response = wrapped.response(method, url, data, headers);
958+
xhr.$$respHeaders = response[2];
959+
callback(response[0], response[1], xhr.getAllResponseHeaders());
960+
}
961+
962+
function handleTimeout() {
963+
for (var i = 0, ii = responses.length; i < ii; i++) {
964+
if (responses[i] === handleResponse) {
965+
responses.splice(i, 1);
966+
callback(-1, undefined, '');
967+
break;
968+
}
969+
}
970+
}
971+
}
972+
951973
if (expectation && expectation.match(method, url)) {
952974
if (!expectation.matchData(data))
953975
throw Error('Expected ' + expectation + ' with different data\n' +
@@ -961,11 +983,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
961983
expectations.shift();
962984

963985
if (expectation.response) {
964-
responses.push(function() {
965-
var response = expectation.response(method, url, data, headers);
966-
xhr.$$respHeaders = response[2];
967-
callback(response[0], response[1], xhr.getAllResponseHeaders());
968-
});
986+
responses.push(wrapResponse(expectation));
969987
return;
970988
}
971989
wasExpected = true;
@@ -976,13 +994,9 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
976994
if (definition.match(method, url, data, headers || {})) {
977995
if (definition.response) {
978996
// if $browser specified, we do auto flush all requests
979-
($browser ? $browser.defer : responsesPush)(function() {
980-
var response = definition.response(method, url, data, headers);
981-
xhr.$$respHeaders = response[2];
982-
callback(response[0], response[1], xhr.getAllResponseHeaders());
983-
});
997+
($browser ? $browser.defer : responsesPush)(wrapResponse(definition));
984998
} else if (definition.passThrough) {
985-
$delegate(method, url, data, callback, headers);
999+
$delegate(method, url, data, callback, headers, timeout);
9861000
} else throw Error('No response defined !');
9871001
return;
9881002
}

src/ngResource/resource.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@
8585
* GET request, otherwise if a cache instance built with
8686
* {@link ng.$cacheFactory $cacheFactory}, this cache will be used for
8787
* caching.
88-
* - **`timeout`** – `{number}` – timeout in milliseconds.
88+
* - **`timeout`** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} that
89+
* should abort the request when resolved.
8990
* - **`withCredentials`** - `{boolean}` - whether to to set the `withCredentials` flag on the
9091
* XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5
9192
* requests with credentials} for more information.

test/ng/httpBackendSpec.js

+38
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,44 @@ describe('$httpBackend', function() {
117117
});
118118

119119

120+
it('should abort request on timeout promise resolution', inject(function($timeout) {
121+
callback.andCallFake(function(status, response) {
122+
expect(status).toBe(-1);
123+
});
124+
125+
$backend('GET', '/url', null, callback, {}, $timeout(noop, 2000));
126+
xhr = MockXhr.$$lastInstance;
127+
spyOn(xhr, 'abort');
128+
129+
$timeout.flush();
130+
expect(xhr.abort).toHaveBeenCalledOnce();
131+
132+
xhr.status = 0;
133+
xhr.readyState = 4;
134+
xhr.onreadystatechange();
135+
expect(callback).toHaveBeenCalledOnce();
136+
}));
137+
138+
139+
it('should not abort resolved request on timeout promise resolution', inject(function($timeout) {
140+
callback.andCallFake(function(status, response) {
141+
expect(status).toBe(200);
142+
});
143+
144+
$backend('GET', '/url', null, callback, {}, $timeout(noop, 2000));
145+
xhr = MockXhr.$$lastInstance;
146+
spyOn(xhr, 'abort');
147+
148+
xhr.status = 200;
149+
xhr.readyState = 4;
150+
xhr.onreadystatechange();
151+
expect(callback).toHaveBeenCalledOnce();
152+
153+
$timeout.flush();
154+
expect(xhr.abort).not.toHaveBeenCalled();
155+
}));
156+
157+
120158
it('should cancel timeout on completion', function() {
121159
callback.andCallFake(function(status, response) {
122160
expect(status).toBe(200);

test/ng/httpSpec.js

+27
Original file line numberDiff line numberDiff line change
@@ -1273,6 +1273,33 @@ describe('$http', function() {
12731273
});
12741274

12751275

1276+
describe('timeout', function() {
1277+
1278+
it('should abort requests when timeout promise resolves', inject(function($q) {
1279+
var canceler = $q.defer();
1280+
1281+
$httpBackend.expect('GET', '/some').respond(200);
1282+
1283+
$http({method: 'GET', url: '/some', timeout: canceler.promise}).error(
1284+
function(data, status, headers, config) {
1285+
expect(data).toBeUndefined();
1286+
expect(status).toBe(0);
1287+
expect(headers()).toEqual({});
1288+
expect(config.url).toBe('/some');
1289+
callback();
1290+
});
1291+
1292+
$rootScope.$apply(function() {
1293+
canceler.resolve();
1294+
});
1295+
1296+
expect(callback).toHaveBeenCalled();
1297+
$httpBackend.verifyNoOutstandingExpectation();
1298+
$httpBackend.verifyNoOutstandingRequest();
1299+
}));
1300+
});
1301+
1302+
12761303
describe('pendingRequests', function() {
12771304

12781305
it('should be an array of pending requests', function() {

test/ngMock/angular-mocksSpec.js

+20-2
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,24 @@ describe('ngMock', function() {
798798
});
799799

800800

801+
it('should abort requests when timeout promise resolves', function() {
802+
hb.expect('GET', '/url1').respond(200);
803+
804+
var canceler, then = jasmine.createSpy('then').andCallFake(function(fn) {
805+
canceler = fn;
806+
});
807+
808+
hb('GET', '/url1', null, callback, null, {then: then});
809+
expect(typeof canceler).toBe('function');
810+
811+
canceler(); // simulate promise resolution
812+
813+
expect(callback).toHaveBeenCalledWith(-1, undefined, '');
814+
hb.verifyNoOutstandingExpectation();
815+
hb.verifyNoOutstandingRequest();
816+
});
817+
818+
801819
it('should throw an exception if no response defined', function() {
802820
hb.when('GET', '/test');
803821
expect(function() {
@@ -1006,8 +1024,8 @@ describe('ngMockE2E', function() {
10061024
hb.when('GET', /\/passThrough\/.*/).passThrough();
10071025
hb('GET', '/passThrough/23', null, callback);
10081026

1009-
expect(realHttpBackend).
1010-
toHaveBeenCalledOnceWith('GET', '/passThrough/23', null, callback, undefined);
1027+
expect(realHttpBackend).toHaveBeenCalledOnceWith(
1028+
'GET', '/passThrough/23', null, callback, undefined, undefined);
10111029
});
10121030
});
10131031

0 commit comments

Comments
 (0)