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
This commit is contained in:
committed by
Georgios Kalpakas
parent
122d89b240
commit
e872f0ed36
+10
-8
@@ -453,6 +453,7 @@ function $HttpProvider() {
|
||||
* - **headers** – `{function([headerName])}` – Header getter function.
|
||||
* - **config** – `{Object}` – The configuration object that was used to generate the request.
|
||||
* - **statusText** – `{string}` – HTTP status text of the response.
|
||||
* - **xhrStatus** – `{string}` – Status of the XMLHttpRequest (`complete`, `error`, `timeout` or `abort`).
|
||||
*
|
||||
* A response status code between 200 and 299 is considered a success status and will result in
|
||||
* the success callback being called. Any response status code outside of that range is
|
||||
@@ -1294,9 +1295,9 @@ function $HttpProvider() {
|
||||
} else {
|
||||
// serving from cache
|
||||
if (isArray(cachedResp)) {
|
||||
resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3]);
|
||||
resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3], cachedResp[4]);
|
||||
} else {
|
||||
resolvePromise(cachedResp, 200, {}, 'OK');
|
||||
resolvePromise(cachedResp, 200, {}, 'OK', 'complete');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1353,10 +1354,10 @@ function $HttpProvider() {
|
||||
* - resolves the raw $http promise
|
||||
* - calls $apply
|
||||
*/
|
||||
function done(status, response, headersString, statusText) {
|
||||
function done(status, response, headersString, statusText, xhrStatus) {
|
||||
if (cache) {
|
||||
if (isSuccess(status)) {
|
||||
cache.put(url, [status, response, parseHeaders(headersString), statusText]);
|
||||
cache.put(url, [status, response, parseHeaders(headersString), statusText, xhrStatus]);
|
||||
} else {
|
||||
// remove promise from the cache
|
||||
cache.remove(url);
|
||||
@@ -1364,7 +1365,7 @@ function $HttpProvider() {
|
||||
}
|
||||
|
||||
function resolveHttpPromise() {
|
||||
resolvePromise(response, status, headersString, statusText);
|
||||
resolvePromise(response, status, headersString, statusText, xhrStatus);
|
||||
}
|
||||
|
||||
if (useApplyAsync) {
|
||||
@@ -1379,7 +1380,7 @@ function $HttpProvider() {
|
||||
/**
|
||||
* Resolves the raw $http promise.
|
||||
*/
|
||||
function resolvePromise(response, status, headers, statusText) {
|
||||
function resolvePromise(response, status, headers, statusText, xhrStatus) {
|
||||
//status: HTTP response status code, 0, -1 (aborted by timeout / promise)
|
||||
status = status >= -1 ? status : 0;
|
||||
|
||||
@@ -1388,12 +1389,13 @@ function $HttpProvider() {
|
||||
status: status,
|
||||
headers: headersGetter(headers),
|
||||
config: config,
|
||||
statusText: statusText
|
||||
statusText: statusText,
|
||||
xhrStatus: xhrStatus
|
||||
});
|
||||
}
|
||||
|
||||
function resolvePromiseWithResult(result) {
|
||||
resolvePromise(result.data, result.status, shallowCopy(result.headers()), result.statusText);
|
||||
resolvePromise(result.data, result.status, shallowCopy(result.headers()), result.statusText, result.xhrStatus);
|
||||
}
|
||||
|
||||
function removePendingReq() {
|
||||
|
||||
+18
-7
@@ -64,7 +64,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
|
||||
var jsonpDone = jsonpReq(url, callbackPath, function(status, text) {
|
||||
// jsonpReq only ever sets status to 200 (OK), 404 (ERROR) or -1 (WAITING)
|
||||
var response = (status === 200) && callbacks.getResponse(callbackPath);
|
||||
completeRequest(callback, status, response, '', text);
|
||||
completeRequest(callback, status, response, '', text, 'complete');
|
||||
callbacks.removeCallback(callbackPath);
|
||||
});
|
||||
} else {
|
||||
@@ -99,18 +99,29 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
|
||||
status,
|
||||
response,
|
||||
xhr.getAllResponseHeaders(),
|
||||
statusText);
|
||||
statusText,
|
||||
'complete');
|
||||
};
|
||||
|
||||
var requestError = function() {
|
||||
// The response is always empty
|
||||
// See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error
|
||||
completeRequest(callback, -1, null, null, '');
|
||||
completeRequest(callback, -1, null, null, '', 'error');
|
||||
};
|
||||
|
||||
var requestAborted = function() {
|
||||
completeRequest(callback, -1, null, null, '', 'abort');
|
||||
};
|
||||
|
||||
var requestTimeout = function() {
|
||||
// The response is always empty
|
||||
// See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error
|
||||
completeRequest(callback, -1, null, null, '', 'timeout');
|
||||
};
|
||||
|
||||
xhr.onerror = requestError;
|
||||
xhr.onabort = requestError;
|
||||
xhr.ontimeout = requestError;
|
||||
xhr.onabort = requestAborted;
|
||||
xhr.ontimeout = requestTimeout;
|
||||
|
||||
forEach(eventHandlers, function(value, key) {
|
||||
xhr.addEventListener(key, value);
|
||||
@@ -160,14 +171,14 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
|
||||
}
|
||||
}
|
||||
|
||||
function completeRequest(callback, status, response, headersString, statusText) {
|
||||
function completeRequest(callback, status, response, headersString, statusText, xhrStatus) {
|
||||
// cancel timeout and subsequent timeout promise resolution
|
||||
if (isDefined(timeoutId)) {
|
||||
$browserDefer.cancel(timeoutId);
|
||||
}
|
||||
jsonpDone = xhr = null;
|
||||
|
||||
callback(status, response, headersString, statusText);
|
||||
callback(status, response, headersString, statusText, xhrStatus);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Vendored
+4
-4
@@ -1354,8 +1354,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
|
||||
return function() {
|
||||
return angular.isNumber(status)
|
||||
? [status, data, headers, statusText]
|
||||
: [200, status, data, headers];
|
||||
? [status, data, headers, statusText, 'complete']
|
||||
: [200, status, data, headers, 'complete'];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1391,14 +1391,14 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
var response = wrapped.response(method, url, data, headers, wrapped.params(url));
|
||||
xhr.$$respHeaders = response[2];
|
||||
callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders(),
|
||||
copy(response[3] || ''));
|
||||
copy(response[3] || ''), copy(response[4]));
|
||||
}
|
||||
|
||||
function handleTimeout() {
|
||||
for (var i = 0, ii = responses.length; i < ii; i++) {
|
||||
if (responses[i] === handleResponse) {
|
||||
responses.splice(i, 1);
|
||||
callback(-1, undefined, '');
|
||||
callback(-1, undefined, '', undefined, 'timeout');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,11 +174,12 @@ describe('$httpBackend', function() {
|
||||
});
|
||||
|
||||
it('should complete the request on timeout', function() {
|
||||
callback.and.callFake(function(status, response, headers, statusText) {
|
||||
callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
|
||||
expect(status).toBe(-1);
|
||||
expect(response).toBe(null);
|
||||
expect(headers).toBe(null);
|
||||
expect(statusText).toBe('');
|
||||
expect(xhrStatus).toBe('timeout');
|
||||
});
|
||||
$backend('GET', '/url', null, callback, {});
|
||||
xhr = MockXhr.$$lastInstance;
|
||||
@@ -189,6 +190,60 @@ describe('$httpBackend', function() {
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should complete the request on abort', function() {
|
||||
callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
|
||||
expect(status).toBe(-1);
|
||||
expect(response).toBe(null);
|
||||
expect(headers).toBe(null);
|
||||
expect(statusText).toBe('');
|
||||
expect(xhrStatus).toBe('abort');
|
||||
});
|
||||
$backend('GET', '/url', null, callback, {});
|
||||
xhr = MockXhr.$$lastInstance;
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
xhr.onabort();
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should complete the request on error', function() {
|
||||
callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
|
||||
expect(status).toBe(-1);
|
||||
expect(response).toBe(null);
|
||||
expect(headers).toBe(null);
|
||||
expect(statusText).toBe('');
|
||||
expect(xhrStatus).toBe('error');
|
||||
});
|
||||
$backend('GET', '/url', null, callback, {});
|
||||
xhr = MockXhr.$$lastInstance;
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
xhr.onerror();
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should complete the request on success', function() {
|
||||
callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
|
||||
expect(status).toBe(200);
|
||||
expect(response).toBe('response');
|
||||
expect(headers).toBe('');
|
||||
expect(statusText).toBe('');
|
||||
expect(xhrStatus).toBe('complete');
|
||||
});
|
||||
$backend('GET', '/url', null, callback, {});
|
||||
xhr = MockXhr.$$lastInstance;
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
xhr.statusText = '';
|
||||
xhr.response = 'response';
|
||||
xhr.status = 200;
|
||||
xhr.onload();
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should abort request on timeout', function() {
|
||||
callback.and.callFake(function(status, response) {
|
||||
expect(status).toBe(-1);
|
||||
|
||||
@@ -448,6 +448,28 @@ describe('$http', function() {
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should pass xhrStatus in response object when a request is successful', function() {
|
||||
$httpBackend.expect('GET', '/url').respond(200, 'SUCCESS', {}, 'OK');
|
||||
$http({url: '/url', method: 'GET'}).then(function(response) {
|
||||
expect(response.xhrStatus).toBe('complete');
|
||||
callback();
|
||||
});
|
||||
|
||||
$httpBackend.flush();
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should pass xhrStatus in response object when a request fails', function() {
|
||||
$httpBackend.expect('GET', '/url').respond(404, 'ERROR', {}, 'Not Found');
|
||||
$http({url: '/url', method: 'GET'}).then(null, function(response) {
|
||||
expect(response.xhrStatus).toBe('complete');
|
||||
callback();
|
||||
});
|
||||
|
||||
$httpBackend.flush();
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
|
||||
it('should pass in the response object when a request failed', function() {
|
||||
$httpBackend.expect('GET', '/url').respond(543, 'bad error', {'request-id': '123'});
|
||||
@@ -1623,6 +1645,17 @@ describe('$http', function() {
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
}));
|
||||
|
||||
it('should cache xhrStatus as well', inject(function($rootScope) {
|
||||
doFirstCacheRequest('GET', 201, null);
|
||||
callback.and.callFake(function(response) {
|
||||
expect(response.xhrStatus).toBe('complete');
|
||||
});
|
||||
|
||||
$http({method: 'get', url: '/url', cache: cache}).then(callback);
|
||||
$rootScope.$digest();
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
}));
|
||||
|
||||
|
||||
it('should use cache even if second request was made before the first returned', function() {
|
||||
$httpBackend.expect('GET', '/url').respond(201, 'fake-response');
|
||||
@@ -1788,6 +1821,7 @@ describe('$http', function() {
|
||||
function(response) {
|
||||
expect(response.data).toBeUndefined();
|
||||
expect(response.status).toBe(-1);
|
||||
expect(response.xhrStatus).toBe('timeout');
|
||||
expect(response.headers()).toEqual(Object.create(null));
|
||||
expect(response.config.url).toBe('/some');
|
||||
callback();
|
||||
|
||||
Vendored
+32
-20
@@ -1343,8 +1343,8 @@ describe('ngMock', function() {
|
||||
hb.flush();
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(2);
|
||||
expect(callback.calls.argsFor(0)).toEqual([201, 'second', '', '']);
|
||||
expect(callback.calls.argsFor(1)).toEqual([200, 'first', '', '']);
|
||||
expect(callback.calls.argsFor(0)).toEqual([201, 'second', '', '', 'complete']);
|
||||
expect(callback.calls.argsFor(1)).toEqual([200, 'first', '', '', 'complete']);
|
||||
});
|
||||
|
||||
|
||||
@@ -1354,7 +1354,7 @@ describe('ngMock', function() {
|
||||
hb('GET', '/url1', undefined, callback);
|
||||
hb.flush();
|
||||
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'first', 'header: val', 'OK');
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'first', 'header: val', 'OK', 'complete');
|
||||
});
|
||||
|
||||
it('should default status code to 200', function() {
|
||||
@@ -1377,7 +1377,19 @@ describe('ngMock', function() {
|
||||
hb('GET', '/url1', null, callback);
|
||||
hb.flush();
|
||||
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'first', 'header: val', 'OK');
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'first', 'header: val', 'OK', 'complete');
|
||||
});
|
||||
|
||||
it('should default xhrStatus to complete', function() {
|
||||
callback.and.callFake(function(status, response, headers, x, xhrStatus) {
|
||||
expect(xhrStatus).toBe('complete');
|
||||
});
|
||||
|
||||
hb.expect('GET', '/url1').respond('some-data');
|
||||
hb('GET', '/url1', null, callback);
|
||||
|
||||
hb.flush();
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should take function', function() {
|
||||
@@ -1388,7 +1400,7 @@ describe('ngMock', function() {
|
||||
hb('GET', '/some?q=s', 'data', callback, {a: 'b'});
|
||||
hb.flush();
|
||||
|
||||
expect(callback).toHaveBeenCalledOnceWith(301, 'GET/some?q=s;data;a=b;q=s', 'Connection: keep-alive', 'Moved Permanently');
|
||||
expect(callback).toHaveBeenCalledOnceWith(301, 'GET/some?q=s;data;a=b;q=s', 'Connection: keep-alive', 'Moved Permanently', undefined);
|
||||
});
|
||||
|
||||
it('should decode query parameters in respond() function', function() {
|
||||
@@ -1400,7 +1412,7 @@ describe('ngMock', function() {
|
||||
hb('GET', '/url?query=l%E2%80%A2ng%20string%20w%2F%20spec%5Eal%20char%24&id=1234&orderBy=-name', null, callback);
|
||||
hb.flush();
|
||||
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'id=1234;orderBy=-name;query=l•ng string w/ spec^al char$', '', '');
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'id=1234;orderBy=-name;query=l•ng string w/ spec^al char$', '', '', undefined);
|
||||
});
|
||||
|
||||
it('should include regex captures in respond() params when keys provided', function() {
|
||||
@@ -1412,7 +1424,7 @@ describe('ngMock', function() {
|
||||
hb('GET', '/1234/article/cool-angular-article', null, callback);
|
||||
hb.flush();
|
||||
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'id=1234;name=cool-angular-article', '', '');
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'id=1234;name=cool-angular-article', '', '', undefined);
|
||||
});
|
||||
|
||||
it('should default response headers to ""', function() {
|
||||
@@ -1425,8 +1437,8 @@ describe('ngMock', function() {
|
||||
hb.flush();
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(2);
|
||||
expect(callback.calls.argsFor(0)).toEqual([200, 'first', '', '']);
|
||||
expect(callback.calls.argsFor(1)).toEqual([200, 'second', '', '']);
|
||||
expect(callback.calls.argsFor(0)).toEqual([200, 'first', '', '', 'complete']);
|
||||
expect(callback.calls.argsFor(1)).toEqual([200, 'second', '', '', 'complete']);
|
||||
});
|
||||
|
||||
it('should be able to override response of expect definition', function() {
|
||||
@@ -1436,7 +1448,7 @@ describe('ngMock', function() {
|
||||
|
||||
hb('GET', '/url1', null, callback);
|
||||
hb.flush();
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'second', '', '');
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'second', '', '', 'complete');
|
||||
});
|
||||
|
||||
it('should be able to override response of when definition', function() {
|
||||
@@ -1446,7 +1458,7 @@ describe('ngMock', function() {
|
||||
|
||||
hb('GET', '/url1', null, callback);
|
||||
hb.flush();
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'second', '', '');
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'second', '', '', 'complete');
|
||||
});
|
||||
|
||||
it('should be able to override response of expect definition with chaining', function() {
|
||||
@@ -1455,7 +1467,7 @@ describe('ngMock', function() {
|
||||
|
||||
hb('GET', '/url1', null, callback);
|
||||
hb.flush();
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'second', '', '');
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'second', '', '', 'complete');
|
||||
});
|
||||
|
||||
it('should be able to override response of when definition with chaining', function() {
|
||||
@@ -1464,7 +1476,7 @@ describe('ngMock', function() {
|
||||
|
||||
hb('GET', '/url1', null, callback);
|
||||
hb.flush();
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'second', '', '');
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'second', '', '', 'complete');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1657,7 +1669,7 @@ describe('ngMock', function() {
|
||||
|
||||
canceler(); // simulate promise resolution
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(-1, undefined, '');
|
||||
expect(callback).toHaveBeenCalledWith(-1, undefined, '', undefined, 'timeout');
|
||||
hb.verifyNoOutstandingExpectation();
|
||||
hb.verifyNoOutstandingRequest();
|
||||
});
|
||||
@@ -1669,7 +1681,7 @@ describe('ngMock', function() {
|
||||
hb('GET', '/url1', null, callback, null, 200);
|
||||
$timeout.flush(300);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(-1, undefined, '');
|
||||
expect(callback).toHaveBeenCalledWith(-1, undefined, '', undefined, 'timeout');
|
||||
hb.verifyNoOutstandingExpectation();
|
||||
hb.verifyNoOutstandingRequest();
|
||||
}));
|
||||
@@ -1831,7 +1843,7 @@ describe('ngMock', function() {
|
||||
hb[shortcut]('/foo').respond('bar');
|
||||
hb(method, '/foo', undefined, callback);
|
||||
hb.flush();
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'bar', '', '');
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'bar', '', '', 'complete');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1846,7 +1858,7 @@ describe('ngMock', function() {
|
||||
hb[routeShortcut](this, '/route').respond('path');
|
||||
hb(this, '/route', undefined, callback);
|
||||
hb.flush();
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'path', '', '');
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'path', '', '', 'complete');
|
||||
}
|
||||
);
|
||||
they('should match colon delimited parameters in ' + routeShortcut + ' $prop method', methods,
|
||||
@@ -1854,7 +1866,7 @@ describe('ngMock', function() {
|
||||
hb[routeShortcut](this, '/route/:id/path/:s_id').respond('path');
|
||||
hb(this, '/route/123/path/456', undefined, callback);
|
||||
hb.flush();
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'path', '', '');
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'path', '', '', 'complete');
|
||||
}
|
||||
);
|
||||
they('should ignore query param when matching in ' + routeShortcut + ' $prop method', methods,
|
||||
@@ -1862,7 +1874,7 @@ describe('ngMock', function() {
|
||||
hb[routeShortcut](this, '/route/:id').respond('path');
|
||||
hb(this, '/route/123?q=str&foo=bar', undefined, callback);
|
||||
hb.flush();
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'path', '', '');
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'path', '', '', 'complete');
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -2462,7 +2474,7 @@ describe('ngMockE2E', function() {
|
||||
$browser.defer.flush();
|
||||
|
||||
expect(realHttpBackend).not.toHaveBeenCalled();
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'passThrough override', '', '');
|
||||
expect(callback).toHaveBeenCalledOnceWith(200, 'passThrough override', '', '', 'complete');
|
||||
}));
|
||||
|
||||
it('should pass through to an httpBackend that uses the same $browser service', inject(function($browser) {
|
||||
|
||||
Reference in New Issue
Block a user