From b0a9c64cb8183ad150dd055ac691eb3f354f38f1 Mon Sep 17 00:00:00 2001 From: jim lyndon Date: Thu, 18 Jul 2013 02:08:38 +1000 Subject: [PATCH] feat($http): add xhr statusText to completeRequest callback Makes xhr status text accessible is $http success/error callback. See www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-statustext Closes #2335 --- src/ng/http.js | 16 +++++++++------- src/ng/httpBackend.js | 23 ++++++++++++++++++----- src/ngMock/angular-mocks.js | 32 ++++++++++++++++---------------- test/ng/httpBackendSpec.js | 10 ++++++++-- test/ngMock/angular-mocksSpec.js | 20 ++++++++++---------- 5 files changed, 61 insertions(+), 40 deletions(-) diff --git a/src/ng/http.js b/src/ng/http.js index a44da3a431b0..81131d123490 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -570,6 +570,7 @@ function $HttpProvider() { * - **status** – `{number}` – HTTP status code of the response. * - **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. * * @property {Array.} pendingRequests Array of config objects for currently pending * requests. This is primarily meant to be used for debugging purposes. @@ -936,9 +937,9 @@ function $HttpProvider() { } else { // serving from cache if (isArray(cachedResp)) { - resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2])); + resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2]), cachedResp[3]); } else { - resolvePromise(cachedResp, 200, {}); + resolvePromise(cachedResp, 200, {}, 'OK'); } } } else { @@ -962,17 +963,17 @@ function $HttpProvider() { * - resolves the raw $http promise * - calls $apply */ - function done(status, response, headersString) { + function done(status, response, headersString, statusText) { if (cache) { if (isSuccess(status)) { - cache.put(url, [status, response, parseHeaders(headersString)]); + cache.put(url, [status, response, parseHeaders(headersString), statusText]); } else { // remove promise from the cache cache.remove(url); } } - resolvePromise(response, status, headersString); + resolvePromise(response, status, headersString, statusText); if (!$rootScope.$$phase) $rootScope.$apply(); } @@ -980,7 +981,7 @@ function $HttpProvider() { /** * Resolves the raw $http promise. */ - function resolvePromise(response, status, headers) { + function resolvePromise(response, status, headers, statusText) { // normalize internal statuses to 0 status = Math.max(status, 0); @@ -988,7 +989,8 @@ function $HttpProvider() { data: response, status: status, headers: headersGetter(headers), - config: config + config: config, + statusText : statusText }); } diff --git a/src/ng/httpBackend.js b/src/ng/httpBackend.js index 69711df23bb2..7da5112bec7b 100644 --- a/src/ng/httpBackend.js +++ b/src/ng/httpBackend.js @@ -91,7 +91,8 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument, completeRequest(callback, status || xhr.status, (xhr.responseType ? xhr.response : xhr.responseText), - responseHeaders); + responseHeaders, + xhr.statusText || ''); } }; @@ -119,7 +120,7 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument, xhr && xhr.abort(); } - function completeRequest(callback, status, response, headersString) { + function completeRequest(callback, status, response, headersString, statusText) { // URL_MATCH is defined in src/service/location.js var protocol = (url.match(SERVER_MATCH) || ['', locationProtocol])[1]; @@ -128,12 +129,24 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument, jsonpDone = xhr = null; // fix status code for file protocol (it's always 0) - status = (protocol == 'file') ? (response ? 200 : 404) : status; + if (protocol == 'file') { + if (response) { + status = 200; + statusText = 'OK'; + } + else { + status = 404; + statusText = 'Not Found'; + } + } // normalize IE bug (http://bugs.jquery.com/ticket/1450) - status = status == 1223 ? 204 : status; + if (status == 1223) { + status = 204; + statusText = 'No Content'; + } - callback(status, response, headersString); + callback(status, response, headersString, statusText); $browser.$$completeOutstandingRequest(noop); } }; diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index eb7868c9a6cd..9dbc9a19221d 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -952,12 +952,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { responses = [], responsesPush = angular.bind(responses, responses.push); - function createResponse(status, data, headers) { + function createResponse(status, data, headers, statusText) { if (angular.isFunction(status)) return status; return function() { return angular.isNumber(status) - ? [status, data, headers] + ? [status, data, headers, statusText] : [200, status, data]; }; } @@ -982,7 +982,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { function handleResponse() { var response = wrapped.response(method, url, data, headers); xhr.$$respHeaders = response[2]; - callback(response[0], response[1], xhr.getAllResponseHeaders()); + callback(response[0], response[1], xhr.getAllResponseHeaders(), response[3] || ''); } function handleTimeout() { @@ -1047,16 +1047,16 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. * - * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` + * - respond – `{function([status,] data[, headers, statusText])|function(function(method, url, data, headers)}` * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string) and response headers - * (Object). + * an array containing response status (number), response data (string), response headers (Object), and the + * text for the status (string). */ $httpBackend.when = function(method, url, data, headers) { var definition = new MockHttpExpectation(method, url, data, headers), chain = { - respond: function(status, data, headers) { - definition.response = createResponse(status, data, headers); + respond: function(status, data, headers, statusText) { + definition.response = createResponse(status, data, headers, statusText); } }; @@ -1166,17 +1166,17 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. * - * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` + * - respond – `{function([status,] data[, headers, statusText])|function(function(method, url, data, headers)}` * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string) and response headers - * (Object). + * an array containing response status (number), response data (string), response headers (Object), and the + * text for the status (string). */ $httpBackend.expect = function(method, url, data, headers) { var expectation = new MockHttpExpectation(method, url, data, headers); expectations.push(expectation); return { - respond: function(status, data, headers) { - expectation.response = createResponse(status, data, headers); + respond: function (status, data, headers, statusText) { + expectation.response = createResponse(status, data, headers, statusText); } }; }; @@ -1622,10 +1622,10 @@ angular.module('ngMockE2E', ['ng']).config(function($provide) { * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. * - * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` + * - respond – `{function([status,] data[, headers, statusText])|function(function(method, url, data, headers)}` * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string) and response headers - * (Object). + * an array containing response status (number), response data (string), response headers (Object), and the + * text for the status (string). * - passThrough – `{function()}` – Any request matching a backend definition with `passThrough` * handler, will be pass through to the real backend (an XHR request will be made to the * server. diff --git a/test/ng/httpBackendSpec.js b/test/ng/httpBackendSpec.js index c65ab2e17c5b..26bc69ba1a2b 100644 --- a/test/ng/httpBackendSpec.js +++ b/test/ng/httpBackendSpec.js @@ -70,14 +70,16 @@ describe('$httpBackend', function() { it('should normalize IE\'s 1223 status code into 204', function() { - callback.andCallFake(function(status) { + callback.andCallFake(function(status, response, headers, statusText) { expect(status).toBe(204); + expect(statusText).toBe('No Content'); }); $backend('GET', 'URL', null, callback); xhr = MockXhr.$$lastInstance; xhr.status = 1223; + xhr.statusText = 'Unknown'; xhr.readyState = 4; xhr.onreadystatechange(); @@ -351,6 +353,7 @@ describe('$httpBackend', function() { expect(callback).toHaveBeenCalled(); expect(callback.mostRecentCall.args[0]).toBe(200); + expect(callback.mostRecentCall.args[3]).toBe('OK'); }); @@ -362,6 +365,7 @@ describe('$httpBackend', function() { expect(callback).toHaveBeenCalled(); expect(callback.mostRecentCall.args[0]).toBe(200); + expect(callback.mostRecentCall.args[3]).toBe('OK'); }); @@ -373,10 +377,11 @@ describe('$httpBackend', function() { expect(callback).toHaveBeenCalled(); expect(callback.mostRecentCall.args[0]).toBe(404); + expect(callback.mostRecentCall.args[3]).toBe('Not Found'); }); - it('should convert 0 to 200 if content - relative url', function() { + it('should convert 0 to 404 if no content - relative url', function() { $backend = createHttpBackend($browser, MockXhr, null, null, null, 'file'); $backend('GET', '/whatever/index.html', null, callback); @@ -384,6 +389,7 @@ describe('$httpBackend', function() { expect(callback).toHaveBeenCalled(); expect(callback.mostRecentCall.args[0]).toBe(404); + expect(callback.mostRecentCall.args[3]).toBe('Not Found'); }); }); }); diff --git a/test/ngMock/angular-mocksSpec.js b/test/ngMock/angular-mocksSpec.js index 3f82ac572452..f4f85d588d32 100644 --- a/test/ngMock/angular-mocksSpec.js +++ b/test/ngMock/angular-mocksSpec.js @@ -622,29 +622,29 @@ describe('ngMock', function() { hb.flush(); expect(callback.callCount).toBe(2); - expect(callback.argsForCall[0]).toEqual([201, 'second', '']); - expect(callback.argsForCall[1]).toEqual([200, 'first', '']); + expect(callback.argsForCall[0]).toEqual([201, 'second', '', '']); + expect(callback.argsForCall[1]).toEqual([200, 'first', '', '']); }); describe('respond()', function() { it('should take values', function() { - hb.expect('GET', '/url1').respond(200, 'first', {'header': 'val'}); + hb.expect('GET', '/url1').respond(200, 'first', {'header': 'val'}, 'OK'); hb('GET', '/url1', undefined, callback); hb.flush(); - expect(callback).toHaveBeenCalledOnceWith(200, 'first', 'header: val'); + expect(callback).toHaveBeenCalledOnceWith(200, 'first', 'header: val', 'OK'); }); it('should take function', function() { - hb.expect('GET', '/some').respond(function(m, u, d, h) { - return [301, m + u + ';' + d + ';a=' + h.a, {'Connection': 'keep-alive'}]; + hb.expect('GET', '/some').respond(function (m, u, d, h) { + return [301, m + u + ';' + d + ';a=' + h.a, {'Connection': 'keep-alive'}, 'Moved Permanently']; }); hb('GET', '/some', 'data', callback, {a: 'b'}); hb.flush(); - expect(callback).toHaveBeenCalledOnceWith(301, 'GET/some;data;a=b', 'Connection: keep-alive'); + expect(callback).toHaveBeenCalledOnceWith(301, 'GET/some;data;a=b', 'Connection: keep-alive', 'Moved Permanently'); }); it('should default status code to 200', function() { @@ -673,8 +673,8 @@ describe('ngMock', function() { hb.flush(); expect(callback.callCount).toBe(2); - expect(callback.argsForCall[0]).toEqual([200, 'first', '']); - expect(callback.argsForCall[1]).toEqual([200, 'second', '']); + expect(callback.argsForCall[0]).toEqual([200, 'first', '', '']); + expect(callback.argsForCall[1]).toEqual([200, 'second', '', '']); }); }); @@ -943,7 +943,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', '', ''); }); }); });