diff options
| author | Igor Minar | 2011-12-28 09:26:22 -0800 |
|---|---|---|
| committer | Vojta Jina | 2012-01-09 13:17:48 -0800 |
| commit | a13b5ed3bc337a493029815c595b89c39eb95af6 (patch) | |
| tree | 2ca5380d5cf5aea68218280cccda5d0221517454 /src | |
| parent | 63cca9afbcf7a772086eb4582d2f409c39e0ed12 (diff) | |
| download | angular.js-a13b5ed3bc337a493029815c595b89c39eb95af6.tar.bz2 | |
fix($http): fix and cleanup $http and friends
$http:
- use promises internally
- get rid of XhrFuture that was previously used internally
- get rid of $browser.defer calls for async stuff (serving from cache),
promises will take care of asynchronicity
- fix transformation bugs (when caching requested + multiple request
pending + error is returned)
- get rid of native header parsing and instead just lazily parse the
header string
$httpBackend:
- don't return raw/mock XMLHttpRequest object (we don't use it for
anything anymore)
- call the callback with response headers string
mock $httpBackend:
- unify response api for expect and when
- call the callback with response headers string
- changed the expect/when failure error message so that EXPECTED and GOT
values are aligned
Conflicts:
src/service/http.js
test/service/compilerSpec.js
test/service/httpSpec.js
Diffstat (limited to 'src')
| -rw-r--r-- | src/angular-mocks.js | 57 | ||||
| -rw-r--r-- | src/service/http.js | 680 | ||||
| -rw-r--r-- | src/service/httpBackend.js | 40 |
3 files changed, 359 insertions, 418 deletions
diff --git a/src/angular-mocks.js b/src/angular-mocks.js index 16be7038..b4a1cbbc 100644 --- a/src/angular-mocks.js +++ b/src/angular-mocks.js @@ -48,9 +48,7 @@ angular.module.ngMock.$BrowserProvider = function(){ }; }; angular.module.ngMock.$Browser = function() { - var self = this, - expectations = {}, - requests = []; + var self = this; this.isMock = true; self.$$url = "http://server"; @@ -590,6 +588,10 @@ angular.module.ngMock.dump = function(object){ /** * @ngdoc object * @name angular.module.ngMock.$httpBackend + * @describe + * Fake HTTP backend used by the $http service during testing. This implementation can be used to + * respond with static or dynamic responses via the `expect` and `when` apis and their shortcuts + * (`expectGET`, `whenPOST`, etc). */ angular.module.ngMock.$HttpBackendProvider = function() { this.$get = function() { @@ -598,7 +600,13 @@ angular.module.ngMock.$HttpBackendProvider = function() { responses = []; function createResponse(status, data, headers) { - return angular.isNumber(status) ? [status, data, headers] : [200, status, data]; + if (isFunction(status)) return status; + + return function() { + return angular.isNumber(status) + ? [status, data, headers] + : [200, status, data]; + } } // TODO(vojta): change params to: method, url, data, headers, callback @@ -608,28 +616,29 @@ angular.module.ngMock.$HttpBackendProvider = function() { wasExpected = false; function prettyPrint(data) { - if (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) - return data; - return angular.toJson(data); + return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) + ? data + : angular.toJson(data); } if (expectation && expectation.match(method, url)) { if (!expectation.matchData(data)) throw Error('Expected ' + expectation + ' with different data\n' + - 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); + 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); if (!expectation.matchHeaders(headers)) throw Error('Expected ' + expectation + ' with different headers\n' + - 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + prettyPrint(headers)); + 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + prettyPrint(headers)); expectations.shift(); if (expectation.response) { responses.push(function() { - xhr.$$headers = expectation.response[2]; - callback(expectation.response[0], expectation.response[1]); + var response = expectation.response(method, url, data, headers); + xhr.$$respHeaders = response[2]; + callback(response[0], response[1], xhr.getAllResponseHeaders()); }); - return method == 'JSONP' ? undefined : xhr; + return; } wasExpected = true; } @@ -639,12 +648,11 @@ angular.module.ngMock.$HttpBackendProvider = function() { if (definition.match(method, url, data, headers || {})) { if (!definition.response) throw Error('No response defined !'); responses.push(function() { - var response = angular.isFunction(definition.response) ? - definition.response(method, url, data, headers) : definition.response; - xhr.$$headers = response[2]; - callback(response[0], response[1]); + var response = definition.response(method, url, data, headers); + xhr.$$respHeaders = response[2]; + callback(response[0], response[1], xhr.getAllResponseHeaders()); }); - return method == 'JSONP' ? undefined : xhr; + return; } } throw wasExpected ? @@ -658,7 +666,7 @@ angular.module.ngMock.$HttpBackendProvider = function() { definitions.push(definition); return { respond: function(status, data, headers) { - definition.response = angular.isFunction(status) ? status : createResponse(status, data, headers); + definition.response = createResponse(status, data, headers); } }; }; @@ -756,7 +764,8 @@ function MockXhr() { this.$$method = method; this.$$url = url; this.$$async = async; - this.$$headers = {}; + this.$$reqHeaders = {}; + this.$$respHeaders = {}; }; this.send = function(data) { @@ -764,20 +773,20 @@ function MockXhr() { }; this.setRequestHeader = function(key, value) { - this.$$headers[key] = value; + this.$$reqHeaders[key] = value; }; this.getResponseHeader = function(name) { // the lookup must be case insensitive, that's why we try two quick lookups and full scan at last - var header = this.$$headers[name]; + var header = this.$$respHeaders[name]; if (header) return header; name = angular.lowercase(name); - header = this.$$headers[name]; + header = this.$$respHeaders[name]; if (header) return header; header = undefined; - angular.forEach(this.$$headers, function(headerVal, headerName) { + angular.forEach(this.$$respHeaders, function(headerVal, headerName) { if (!header && angular.lowercase(headerName) == name) header = headerVal; }); return header; @@ -786,7 +795,7 @@ function MockXhr() { this.getAllResponseHeaders = function() { var lines = []; - angular.forEach(this.$$headers, function(value, key) { + angular.forEach(this.$$respHeaders, function(value, key) { lines.push(key + ': ' + value); }); return lines.join('\n'); diff --git a/src/service/http.js b/src/service/http.js index e6a42b65..bd8e6e65 100644 --- a/src/service/http.js +++ b/src/service/http.js @@ -4,11 +4,13 @@ * Parse headers into key value object * * @param {string} headers Raw headers as a string - * @returns {Object} Parsed headers as key valu object + * @returns {Object} Parsed headers as key value object */ function parseHeaders(headers) { var parsed = {}, key, val, i; + if (!headers) return parsed; + forEach(headers.split('\n'), function(line) { i = line.indexOf(':'); key = lowercase(trim(line.substr(0, i))); @@ -26,6 +28,34 @@ function parseHeaders(headers) { return parsed; } + +/** + * Returns a function that provides access to parsed headers. + * + * Headers are lazy parsed when first requested. + * @see parseHeaders + * + * @param {(string|Object)} headers Headers to provide access to. + * @returns {function(string=)} Returns a getter function which if called with: + * + * - if called with single an argument returns a single header value or null + * - if called with no arguments returns an object containing all headers. + */ +function headersGetter(headersString) { + var headers = isObject(headersString) ? headersString : undefined; + + return function(name) { + if (!headers) headers = parseHeaders(headersString); + + if (name) { + return headers[lowercase(name)] || null; + } + + return headers; + }; +} + + /** * Chain all given functions * @@ -36,7 +66,7 @@ function parseHeaders(headers) { * @param {*=} param Optional parameter to be passed to all transform functions. * @returns {*} Transformed data. */ -function transform(data, fns, param) { +function transformData(data, fns, param) { if (isFunction(fns)) return fns(data); @@ -48,13 +78,18 @@ function transform(data, fns, param) { } +function isSuccess(status) { + return 200 <= status && status < 300; +} + + function $HttpProvider() { var JSON_START = /^\s*(\[|\{[^\{])/, JSON_END = /[\}\]]\s*$/, PROTECTION_PREFIX = /^\)\]\}',?\n/; var $config = this.defaults = { - // transform in-coming reponse data + // transform incoming response data transformResponse: function(data) { if (isString(data)) { // strip json vulnerability protection prefix @@ -65,7 +100,7 @@ function $HttpProvider() { return data; }, - // transform out-going request data + // transform outgoing request data transformRequest: function(d) { return isObject(d) ? toJson(d) : d; }, @@ -81,431 +116,328 @@ function $HttpProvider() { } }; - var responseInterceptors = this.responseInterceptors = []; + var providerResponseInterceptors = this.responseInterceptors = []; this.$get = ['$httpBackend', '$browser', '$exceptionHandler', '$cacheFactory', '$rootScope', '$q', '$injector', function($httpBackend, $browser, $exceptionHandler, $cacheFactory, $rootScope, $q, $injector) { - var defaultCache = $cacheFactory('$http'); + var defaultCache = $cacheFactory('$http'), + responseInterceptors = []; - forEach(responseInterceptors, function(interceptor, index) { - if (isString(interceptor)) { - responseInterceptors[index] = $injector.get(interceptor); - } - }); + forEach(providerResponseInterceptors, function(interceptor) { + responseInterceptors.push(isString(interceptor) ? $injector.get(interceptor) : interceptor); + }); - /** - * @ngdoc function - * @name angular.module.ng.$http - * @requires $httpBacked - * @requires $browser - * @requires $exceptionHandler - * @requires $cacheFactory - * - * @param {object} config Object describing the request to be made and how it should be processed. - * The object has following properties: - * - * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) - * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. - * - **data** – `{string|Object}` – Data to be sent as the request message data. - * - **headers** – `{Object}` – Map of strings representing HTTP headers to send to the server. - * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the - * GET request, otherwise if a cache instance built with $cacheFactory, this cache will be - * used for caching. - * - * @returns {HttpPromise} Returns a 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: - * - * - **data** – `{string|Object}` – The response body transformed with the transform functions. - * - **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. - * - * @property {Array.<Object>} pendingRequests Array of config objects for pending requests. - * This is primarily meant to be used for debugging purposes. - * - * @description - * $http is a service through which XHR and JSONP requests can be made. - */ - function $http(config) { - var req = new XhrFuture().send(config), - deferredResp = $q.defer(), - promise = deferredResp.promise; - - forEach(responseInterceptors, function(interceptor) { - promise = interceptor(promise); - }); + /** + * @ngdoc function + * @name angular.module.ng.$http + * @requires $httpBacked + * @requires $browser + * @requires $exceptionHandler //TODO(i): still needed? + * @requires $cacheFactory + * + * @param {object} config Object describing the request to be made and how it should be processed. + * The object has following properties: + * + * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) + * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. + * - **data** – `{string|Object}` – Data to be sent as the request message data. + * - **headers** – `{Object}` – Map of strings representing HTTP headers to send to the server. + * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the + * GET request, otherwise if a cache instance built with $cacheFactory, this cache will be + * used for caching. + * + * @returns {HttpPromise} Returns a 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: + * + * - **data** – `{string|Object}` – The response body transformed with the transform functions. + * - **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. + * + * @property {Array.<Object>} pendingRequests Array of config objects for pending requests. + * This is primarily meant to be used for debugging purposes. + * + * @description + * $http is a service through which XHR and JSONP requests can be made. + */ + function $http(config) { + config.method = uppercase(config.method); - promise.success = function(fn) { - promise.then(function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; + var reqTransformFn = config.transformRequest || $config.transformRequest, + respTransformFn = config.transformResponse || $config.transformResponse, + reqData = transformData(config.data, reqTransformFn), + defHeaders = $config.headers, + reqHeaders = extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']}, + defHeaders.common, defHeaders[lowercase(config.method)], config.headers), + promise; - promise.error = function(fn) { - promise.then(null, function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; - req.on('success', function(data, status, headers) { - deferredResp.resolve({data: data, status: status, headers: headers, config: config}); - }).on('error', function(data, status, headers) { - deferredResp.reject({data: data, status: status, headers: headers, config: config}); - }); + // send request + promise = sendReq(config, reqData, reqHeaders); + - return promise; - } - - $http.pendingRequests = []; - - /** - * @ngdoc method - * @name angular.module.ng.$http#get - * @methodOf angular.module.ng.$http - * - * @description - * Shortcut method to perform `GET` request - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - - /** - * @ngdoc method - * @name angular.module.ng.$http#delete - * @methodOf angular.module.ng.$http - * - * @description - * Shortcut method to perform `DELETE` request - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - - /** - * @ngdoc method - * @name angular.module.ng.$http#head - * @methodOf angular.module.ng.$http - * - * @description - * Shortcut method to perform `HEAD` request - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object - * @returns {XhrFuture} Future object - */ - - /** - * @ngdoc method - * @name angular.module.ng.$http#patch - * @methodOf angular.module.ng.$http - * - * @description - * Shortcut method to perform `PATCH` request - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - - /** - * @ngdoc method - * @name angular.module.ng.$http#jsonp - * @methodOf angular.module.ng.$http - * - * @description - * Shortcut method to perform `JSONP` request - * - * @param {string} url Relative or absolute URL specifying the destination of the request. - * Should contain `JSON_CALLBACK` string. - * @param {Object=} config Optional configuration object - * @returns {XhrFuture} Future object - */ - createShortMethods('get', 'delete', 'head', 'patch', 'jsonp'); - - /** - * @ngdoc method - * @name angular.module.ng.$http#post - * @methodOf angular.module.ng.$http - * - * @description - * Shortcut method to perform `POST` request - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {*} data Request content - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - - /** - * @ngdoc method - * @name angular.module.ng.$http#put - * @methodOf angular.module.ng.$http - * - * @description - * Shortcut method to perform `PUT` request - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {*} data Request content - * @param {Object=} config Optional configuration object - * @returns {XhrFuture} Future object - */ - createShortMethodsWithData('post', 'put'); - - return $http; - - function createShortMethods(names) { - forEach(arguments, function(name) { - $http[name] = function(url, config) { - return $http(extend(config || {}, { - method: name, - url: url - })); + // transform future response + promise = promise.then(transformResponse, transformResponse); + + // apply interceptors + forEach(responseInterceptors, function(interceptor) { + promise = interceptor(promise); + }); + + promise.success = function(fn) { + promise.then(function(response) { + fn(response.data, response.status, response.headers, config); + }); + return promise; }; - }); - } - - function createShortMethodsWithData(name) { - forEach(arguments, function(name) { - $http[name] = function(url, data, config) { - return $http(extend(config || {}, { - method: name, - url: url, - data: data - })); + + promise.error = function(fn) { + promise.then(null, function(response) { + fn(response.data, response.status, response.headers, config); + }); + return promise; }; - }); - } - - /** - * Represents Request object, returned by $http() - * - * !!! ACCESSES CLOSURE VARS: - * $httpBackend, $browser, $config, $log, $rootScope, defaultCache, $http.pendingRequests - */ - function XhrFuture() { - var rawRequest, parsedHeaders, - cfg = {}, callbacks = [], - defHeaders = $config.headers, - self = this; + + return promise; + + function transformResponse(response) { + // make a copy since the response must be cacheable + var resp = extend({}, response, { + data: transformData(response.data, respTransformFn, response.headers) + }); + return (isSuccess(response.status)) + ? resp + : $q.reject(resp); + } + } + + $http.pendingRequests = []; /** - * Callback registered to $httpBackend(): - * - caches the response if desired - * - calls fireCallbacks() - * - clears the reference to raw request object + * @ngdoc method + * @name angular.module.ng.$http#get + * @methodOf angular.module.ng.$http + * + * @description + * Shortcut method to perform `GET` request + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object */ - function done(status, response) { - // aborted request or jsonp - if (!rawRequest) parsedHeaders = {}; - - if (cfg.cache && cfg.method == 'GET') { - var cache = isObject(cfg.cache) && cfg.cache || defaultCache; - if (200 <= status && status < 300) { - parsedHeaders = parsedHeaders || parseHeaders(rawRequest.getAllResponseHeaders()); - cache.put(cfg.url, [status, response, parsedHeaders]); - } else { - // remove future object from cache - cache.remove(cfg.url); - } - } - fireCallbacks(response, status); - // TODO(i): we can't null the rawRequest because we might need to be able to call - // rawRequest.getAllResponseHeaders from a promise - // rawRequest = null; - } + /** + * @ngdoc method + * @name angular.module.ng.$http#delete + * @methodOf angular.module.ng.$http + * + * @description + * Shortcut method to perform `DELETE` request + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ /** - * Fire all registered callbacks for given status code + * @ngdoc method + * @name angular.module.ng.$http#head + * @methodOf angular.module.ng.$http * - * This method when: - * - serving response from real request - * - serving response from cache + * @description + * Shortcut method to perform `HEAD` request * - * It does: - * - transform the response - * - call proper callbacks - * - log errors - * - apply the $scope - * - clear parsed headers + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {XhrFuture} Future object */ - function fireCallbacks(response, status) { - var strStatus = status + ''; - - // transform the response - response = transform(response, cfg.transformResponse || $config.transformResponse, rawRequest); - - var idx; // remove from pending requests - if ((idx = indexOf($http.pendingRequests, cfg)) !== -1) - $http.pendingRequests.splice(idx, 1); - - // normalize internal statuses to 0 - status = Math.max(status, 0); - forEach(callbacks, function(callback) { - if (callback.regexp.test(strStatus)) { - try { - // use local var to call it without context - var fn = callback.fn; - fn(response, status, headers); - } catch(e) { - $exceptionHandler(e); - } - } - }); - $rootScope.$apply(); - parsedHeaders = null; - } + /** + * @ngdoc method + * @name angular.module.ng.$http#patch + * @methodOf angular.module.ng.$http + * + * @description + * Shortcut method to perform `PATCH` request + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ /** - * This is the third argument in any user callback - * @see parseHeaders + * @ngdoc method + * @name angular.module.ng.$http#jsonp + * @methodOf angular.module.ng.$http * - * Return single header value or all headers parsed as object. - * Headers all lazy parsed when first requested. + * @description + * Shortcut method to perform `JSONP` request * - * @param {string=} name Name of header - * @returns {string|Object} + * @param {string} url Relative or absolute URL specifying the destination of the request. + * Should contain `JSON_CALLBACK` string. + * @param {Object=} config Optional configuration object + * @returns {XhrFuture} Future object */ - function headers(name) { - if (name) { - return parsedHeaders ? - parsedHeaders[lowercase(name)] || null : - rawRequest.getResponseHeader(name); - } + createShortMethods('get', 'delete', 'head', 'patch', 'jsonp'); - parsedHeaders = parsedHeaders || parseHeaders(rawRequest.getAllResponseHeaders()); + /** + * @ngdoc method + * @name angular.module.ng.$http#post + * @methodOf angular.module.ng.$http + * + * @description + * Shortcut method to perform `POST` request + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {*} data Request content + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ - return parsedHeaders; + /** + * @ngdoc method + * @name angular.module.ng.$http#put + * @methodOf angular.module.ng.$http + * + * @description + * Shortcut method to perform `PUT` request + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {*} data Request content + * @param {Object=} config Optional configuration object + * @returns {XhrFuture} Future object + */ + createShortMethodsWithData('post', 'put'); + + + return $http; + + + function createShortMethods(names) { + forEach(arguments, function(name) { + $http[name] = function(url, config) { + return $http(extend(config || {}, { + method: name, + url: url + })); + }; + }); } + + function createShortMethodsWithData(name) { + forEach(arguments, function(name) { + $http[name] = function(url, data, config) { + return $http(extend(config || {}, { + method: name, + url: url, + data: data + })); + }; + }); + } + + /** - * Retry the request + * Makes the request * - * @param {Object=} config Optional config object to extend the original configuration - * @returns {HttpPromise} + * !!! ACCESSES CLOSURE VARS: + * $httpBackend, $config, $log, $rootScope, defaultCache, $http.pendingRequests */ - this.retry = function(config) { - if (rawRequest) throw 'Can not retry request. Abort pending request first.'; + function sendReq(config, reqData, reqHeaders) { + var deferred = $q.defer(), + promise = deferred.promise, + cache, + cachedResp; - extend(cfg, config); - cfg.method = uppercase(cfg.method); + $http.pendingRequests.push(config); + promise.then(removePendingReq, removePendingReq); - var data = transform(cfg.data, cfg.transformRequest || $config.transformRequest), - headers = extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']}, - defHeaders.common, defHeaders[lowercase(cfg.method)], cfg.headers); - var cache = isObject(cfg.cache) && cfg.cache || defaultCache, - fromCache; + if (config.cache && config.method == 'GET') { + cache = isObject(config.cache) ? config.cache : defaultCache; + } - if (cfg.cache && cfg.method == 'GET') { - fromCache = cache.get(cfg.url); - if (fromCache) { - if (fromCache instanceof XhrFuture) { - // cached request has already been sent, but there is no reponse yet, + if (cache) { + cachedResp = cache.get(config.url); + if (cachedResp) { + if (cachedResp.then) { + // cached request has already been sent, but there is no response yet, // we need to register callback and fire callbacks when the request is back // note, we have to get the values from cache and perform transformations on them, // as the configurations don't have to be same - fromCache.on('always', function() { - var requestFromCache = cache.get(cfg.url); - parsedHeaders = requestFromCache[2]; - fireCallbacks(requestFromCache[1], requestFromCache[0]); - }); + cachedResp.then(removePendingReq, removePendingReq); + return cachedResp; } else { - // serving from cache - still needs to be async - $browser.defer(function() { - parsedHeaders = fromCache[2]; - fireCallbacks(fromCache[1], fromCache[0]); - }); + // serving from cache + if (isArray(cachedResp)) { + resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2])); + } else { + resolvePromise(cachedResp, 200, {}); + } } } else { - // put future object into cache - cache.put(cfg.url, self); + // put the promise for the non-transformed response into cache as a placeholder + cache.put(config.url, promise); } } - // really send the request - if (!cfg.cache || cfg.method !== 'GET' || !fromCache) { - rawRequest = $httpBackend(cfg.method, cfg.url, data, done, headers, cfg.timeout); + // if we won't have the response in cache, send the request to the backend + if (!cachedResp) { + $httpBackend(config.method, config.url, reqData, done, reqHeaders, config.timeout); } - $http.pendingRequests.push(cfg); - return self; - }; + return promise; - // just alias so that in stack trace we can see send() instead of retry() - this.send = this.retry; - /** - * Abort the request - */ - this.abort = function() { - if (rawRequest) { - rawRequest.abort(); + /** + * Callback registered to $httpBackend(): + * - caches the response if desired + * - resolves the raw $http promise + * - calls $apply + */ + function done(status, response, headersString) { + if (cache) { + if (isSuccess(status)) { + cache.put(config.url, [status, response, parseHeaders(headersString)]); + } else { + // remove promise from the cache + cache.remove(config.url); + } + } + + resolvePromise(response, status, headersString); + $rootScope.$apply(); } - return this; - }; - /** - * Register a callback function based on status code - * Note: all matched callbacks will be called, preserving registered order ! - * - * Internal statuses: - * `-2` = jsonp error - * `-1` = timeout - * `0` = aborted - * - * @example - * .on('2xx', function(){}); - * .on('2x1', function(){}); - * .on('404', function(){}); - * .on('20x,3xx', function(){}); - * .on('success', function(){}); - * .on('error', function(){}); - * .on('always', function(){}); - * .on('timeout', function(){}); - * .on('abort', function(){}); - * - * @param {string} pattern Status code pattern with "x" for any number - * @param {function(*, number, function)} callback Function to be called when response arrives - * @returns {XhrFuture} - */ - this.on = function(pattern, callback) { - var alias = { - success: '2xx', - error: '-2,-1,0,4xx,5xx', - always: 'xxx,xx,x', - timeout: '-1', - abort: '0' - }; - callbacks.push({ - fn: callback, - // create regexp from given pattern - regexp: new RegExp('^(' + (alias[pattern] || pattern).replace(/,/g, '|'). - replace(/x/g, '.') + ')$') - }); + /** + * Resolves the raw $http promise. + */ + function resolvePromise(response, status, headers) { + // normalize internal statuses to 0 + status = Math.max(status, 0); - return this; - }; + (isSuccess(status) ? deferred.resolve : deferred.reject)({ + data: response, + status: status, + headers: headersGetter(headers), + config: config + }); + } - /** - * Configuration object of the request - */ - this.config = cfg; - } -}]; -} + function removePendingReq() { + var idx = indexOf($http.pendingRequests, config); + if (idx !== -1) $http.pendingRequests.splice(idx, 1); + } + } + }]; +} diff --git a/src/service/httpBackend.js b/src/service/httpBackend.js index c64519d9..c3114814 100644 --- a/src/service/httpBackend.js +++ b/src/service/httpBackend.js @@ -14,6 +14,11 @@ var XHR = window.XMLHttpRequest || function() { * @requires $document * * @description + * HTTP backend used by the {@link angular.module.ng.$http service} that delegates to + * XMLHttpRequest object. + * + * During testing this implementation is swapped with {@link angular.module.ngMock.$httpBackend mock + * $httpBackend} which can be trained with responses. */ function $HttpBackendProvider() { this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) { @@ -46,24 +51,21 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, body, locati var xhr = new XHR(); xhr.open(method, url, true); forEach(headers, function(value, key) { - if (value) xhr.setRequestHeader(key, value); + if (value) xhr.setRequestHeader(key, value); }); var status; - xhr.send(post || ''); - // IE6, IE7 bug - does sync when serving from cache - if (xhr.readyState == 4) { - $browserDefer(function() { - completeRequest(callback, status || xhr.status, xhr.responseText); - }, 0); - } else { - xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { - completeRequest(callback, status || xhr.status, xhr.responseText); - } - }; - } + // In IE6 and 7, this might be called synchronously when xhr.send below is called and the + // response is in the cache + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + completeRequest( + callback, status || xhr.status, xhr.responseText, xhr.getAllResponseHeaders()); + } + }; + + xhr.send(post || ''); if (timeout > 0) { $browserDefer(function() { @@ -71,23 +73,21 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, body, locati xhr.abort(); }, timeout); } - - return xhr; } - function completeRequest(callback, status, response) { + + function completeRequest(callback, status, response, headersString) { // URL_MATCH is defined in src/service/location.js var protocol = (url.match(URL_MATCH) || ['', locationProtocol])[1]; // fix status code for file protocol (it's always 0) - status = protocol == 'file' ? (response ? 200 : 404) : status; + status = (protocol == 'file') ? (response ? 200 : 404) : status; // normalize IE bug (http://bugs.jquery.com/ticket/1450) status = status == 1223 ? 204 : status; - callback(status, response); + callback(status, response, headersString); $browser.$$completeOutstandingRequest(noop); } }; } - |
