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

Commit 5e2bc5b

Browse files
frederikprijckgkalpak
authored andcommitted
feat($http): allow differentiation between XHR completion, error, abort, timeout
Previously, it wasn't possible to tell if an `$http`-initiated XMLHttpRequest was completed normally or with an error or it was aborted or timed out. This commit adds a new property on the `response` object (`xhrStatus`) which allows to defferentiate between the possible statuses. Fixes #15924 Closes #15847
1 parent 079c485 commit 5e2bc5b

File tree

6 files changed

+154
-40
lines changed

6 files changed

+154
-40
lines changed

src/ng/http.js

+10-8
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,7 @@ function $HttpProvider() {
453453
* - **headers** – `{function([headerName])}` – Header getter function.
454454
* - **config** – `{Object}` – The configuration object that was used to generate the request.
455455
* - **statusText** – `{string}` – HTTP status text of the response.
456+
* - **xhrStatus** – `{string}` – Status of the XMLHttpRequest (`complete`, `error`, `timeout` or `abort`).
456457
*
457458
* A response status code between 200 and 299 is considered a success status and will result in
458459
* the success callback being called. Any response status code outside of that range is
@@ -1294,9 +1295,9 @@ function $HttpProvider() {
12941295
} else {
12951296
// serving from cache
12961297
if (isArray(cachedResp)) {
1297-
resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3]);
1298+
resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3], cachedResp[4]);
12981299
} else {
1299-
resolvePromise(cachedResp, 200, {}, 'OK');
1300+
resolvePromise(cachedResp, 200, {}, 'OK', 'complete');
13001301
}
13011302
}
13021303
} else {
@@ -1353,18 +1354,18 @@ function $HttpProvider() {
13531354
* - resolves the raw $http promise
13541355
* - calls $apply
13551356
*/
1356-
function done(status, response, headersString, statusText) {
1357+
function done(status, response, headersString, statusText, xhrStatus) {
13571358
if (cache) {
13581359
if (isSuccess(status)) {
1359-
cache.put(url, [status, response, parseHeaders(headersString), statusText]);
1360+
cache.put(url, [status, response, parseHeaders(headersString), statusText, xhrStatus]);
13601361
} else {
13611362
// remove promise from the cache
13621363
cache.remove(url);
13631364
}
13641365
}
13651366

13661367
function resolveHttpPromise() {
1367-
resolvePromise(response, status, headersString, statusText);
1368+
resolvePromise(response, status, headersString, statusText, xhrStatus);
13681369
}
13691370

13701371
if (useApplyAsync) {
@@ -1379,7 +1380,7 @@ function $HttpProvider() {
13791380
/**
13801381
* Resolves the raw $http promise.
13811382
*/
1382-
function resolvePromise(response, status, headers, statusText) {
1383+
function resolvePromise(response, status, headers, statusText, xhrStatus) {
13831384
//status: HTTP response status code, 0, -1 (aborted by timeout / promise)
13841385
status = status >= -1 ? status : 0;
13851386

@@ -1388,12 +1389,13 @@ function $HttpProvider() {
13881389
status: status,
13891390
headers: headersGetter(headers),
13901391
config: config,
1391-
statusText: statusText
1392+
statusText: statusText,
1393+
xhrStatus: xhrStatus
13921394
});
13931395
}
13941396

13951397
function resolvePromiseWithResult(result) {
1396-
resolvePromise(result.data, result.status, shallowCopy(result.headers()), result.statusText);
1398+
resolvePromise(result.data, result.status, shallowCopy(result.headers()), result.statusText, result.xhrStatus);
13971399
}
13981400

13991401
function removePendingReq() {

src/ng/httpBackend.js

+18-7
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
6464
var jsonpDone = jsonpReq(url, callbackPath, function(status, text) {
6565
// jsonpReq only ever sets status to 200 (OK), 404 (ERROR) or -1 (WAITING)
6666
var response = (status === 200) && callbacks.getResponse(callbackPath);
67-
completeRequest(callback, status, response, '', text);
67+
completeRequest(callback, status, response, '', text, 'complete');
6868
callbacks.removeCallback(callbackPath);
6969
});
7070
} else {
@@ -99,18 +99,29 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
9999
status,
100100
response,
101101
xhr.getAllResponseHeaders(),
102-
statusText);
102+
statusText,
103+
'complete');
103104
};
104105

105106
var requestError = function() {
106107
// The response is always empty
107108
// See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error
108-
completeRequest(callback, -1, null, null, '');
109+
completeRequest(callback, -1, null, null, '', 'error');
110+
};
111+
112+
var requestAborted = function() {
113+
completeRequest(callback, -1, null, null, '', 'abort');
114+
};
115+
116+
var requestTimeout = function() {
117+
// The response is always empty
118+
// See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error
119+
completeRequest(callback, -1, null, null, '', 'timeout');
109120
};
110121

111122
xhr.onerror = requestError;
112-
xhr.onabort = requestError;
113-
xhr.ontimeout = requestError;
123+
xhr.onabort = requestAborted;
124+
xhr.ontimeout = requestTimeout;
114125

115126
forEach(eventHandlers, function(value, key) {
116127
xhr.addEventListener(key, value);
@@ -160,14 +171,14 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
160171
}
161172
}
162173

163-
function completeRequest(callback, status, response, headersString, statusText) {
174+
function completeRequest(callback, status, response, headersString, statusText, xhrStatus) {
164175
// cancel timeout and subsequent timeout promise resolution
165176
if (isDefined(timeoutId)) {
166177
$browserDefer.cancel(timeoutId);
167178
}
168179
jsonpDone = xhr = null;
169180

170-
callback(status, response, headersString, statusText);
181+
callback(status, response, headersString, statusText, xhrStatus);
171182
}
172183
};
173184

src/ngMock/angular-mocks.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -1354,8 +1354,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
13541354

13551355
return function() {
13561356
return angular.isNumber(status)
1357-
? [status, data, headers, statusText]
1358-
: [200, status, data, headers];
1357+
? [status, data, headers, statusText, 'complete']
1358+
: [200, status, data, headers, 'complete'];
13591359
};
13601360
}
13611361

@@ -1391,14 +1391,14 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
13911391
var response = wrapped.response(method, url, data, headers, wrapped.params(url));
13921392
xhr.$$respHeaders = response[2];
13931393
callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders(),
1394-
copy(response[3] || ''));
1394+
copy(response[3] || ''), copy(response[4]));
13951395
}
13961396

13971397
function handleTimeout() {
13981398
for (var i = 0, ii = responses.length; i < ii; i++) {
13991399
if (responses[i] === handleResponse) {
14001400
responses.splice(i, 1);
1401-
callback(-1, undefined, '');
1401+
callback(-1, undefined, '', undefined, 'timeout');
14021402
break;
14031403
}
14041404
}

test/ng/httpBackendSpec.js

+56-1
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,12 @@ describe('$httpBackend', function() {
174174
});
175175

176176
it('should complete the request on timeout', function() {
177-
callback.and.callFake(function(status, response, headers, statusText) {
177+
callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
178178
expect(status).toBe(-1);
179179
expect(response).toBe(null);
180180
expect(headers).toBe(null);
181181
expect(statusText).toBe('');
182+
expect(xhrStatus).toBe('timeout');
182183
});
183184
$backend('GET', '/url', null, callback, {});
184185
xhr = MockXhr.$$lastInstance;
@@ -189,6 +190,60 @@ describe('$httpBackend', function() {
189190
expect(callback).toHaveBeenCalledOnce();
190191
});
191192

193+
it('should complete the request on abort', function() {
194+
callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
195+
expect(status).toBe(-1);
196+
expect(response).toBe(null);
197+
expect(headers).toBe(null);
198+
expect(statusText).toBe('');
199+
expect(xhrStatus).toBe('abort');
200+
});
201+
$backend('GET', '/url', null, callback, {});
202+
xhr = MockXhr.$$lastInstance;
203+
204+
expect(callback).not.toHaveBeenCalled();
205+
206+
xhr.onabort();
207+
expect(callback).toHaveBeenCalledOnce();
208+
});
209+
210+
it('should complete the request on error', function() {
211+
callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
212+
expect(status).toBe(-1);
213+
expect(response).toBe(null);
214+
expect(headers).toBe(null);
215+
expect(statusText).toBe('');
216+
expect(xhrStatus).toBe('error');
217+
});
218+
$backend('GET', '/url', null, callback, {});
219+
xhr = MockXhr.$$lastInstance;
220+
221+
expect(callback).not.toHaveBeenCalled();
222+
223+
xhr.onerror();
224+
expect(callback).toHaveBeenCalledOnce();
225+
});
226+
227+
it('should complete the request on success', function() {
228+
callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
229+
expect(status).toBe(200);
230+
expect(response).toBe('response');
231+
expect(headers).toBe('');
232+
expect(statusText).toBe('');
233+
expect(xhrStatus).toBe('complete');
234+
});
235+
$backend('GET', '/url', null, callback, {});
236+
xhr = MockXhr.$$lastInstance;
237+
238+
expect(callback).not.toHaveBeenCalled();
239+
240+
xhr.statusText = '';
241+
xhr.response = 'response';
242+
xhr.status = 200;
243+
xhr.onload();
244+
expect(callback).toHaveBeenCalledOnce();
245+
});
246+
192247
it('should abort request on timeout', function() {
193248
callback.and.callFake(function(status, response) {
194249
expect(status).toBe(-1);

test/ng/httpSpec.js

+34
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,28 @@ describe('$http', function() {
448448
expect(callback).toHaveBeenCalledOnce();
449449
});
450450

451+
it('should pass xhrStatus in response object when a request is successful', function() {
452+
$httpBackend.expect('GET', '/url').respond(200, 'SUCCESS', {}, 'OK');
453+
$http({url: '/url', method: 'GET'}).then(function(response) {
454+
expect(response.xhrStatus).toBe('complete');
455+
callback();
456+
});
457+
458+
$httpBackend.flush();
459+
expect(callback).toHaveBeenCalledOnce();
460+
});
461+
462+
it('should pass xhrStatus in response object when a request fails', function() {
463+
$httpBackend.expect('GET', '/url').respond(404, 'ERROR', {}, 'Not Found');
464+
$http({url: '/url', method: 'GET'}).then(null, function(response) {
465+
expect(response.xhrStatus).toBe('complete');
466+
callback();
467+
});
468+
469+
$httpBackend.flush();
470+
expect(callback).toHaveBeenCalledOnce();
471+
});
472+
451473

452474
it('should pass in the response object when a request failed', function() {
453475
$httpBackend.expect('GET', '/url').respond(543, 'bad error', {'request-id': '123'});
@@ -1623,6 +1645,17 @@ describe('$http', function() {
16231645
expect(callback).toHaveBeenCalledOnce();
16241646
}));
16251647

1648+
it('should cache xhrStatus as well', inject(function($rootScope) {
1649+
doFirstCacheRequest('GET', 201, null);
1650+
callback.and.callFake(function(response) {
1651+
expect(response.xhrStatus).toBe('complete');
1652+
});
1653+
1654+
$http({method: 'get', url: '/url', cache: cache}).then(callback);
1655+
$rootScope.$digest();
1656+
expect(callback).toHaveBeenCalledOnce();
1657+
}));
1658+
16261659

16271660
it('should use cache even if second request was made before the first returned', function() {
16281661
$httpBackend.expect('GET', '/url').respond(201, 'fake-response');
@@ -1788,6 +1821,7 @@ describe('$http', function() {
17881821
function(response) {
17891822
expect(response.data).toBeUndefined();
17901823
expect(response.status).toBe(-1);
1824+
expect(response.xhrStatus).toBe('timeout');
17911825
expect(response.headers()).toEqual(Object.create(null));
17921826
expect(response.config.url).toBe('/some');
17931827
callback();

0 commit comments

Comments
 (0)