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/service/http.js | |
| 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/service/http.js')
| -rw-r--r-- | src/service/http.js | 680 |
1 files changed, 306 insertions, 374 deletions
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); + } + } + }]; +} |
