aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorIgor Minar2011-12-28 09:26:22 -0800
committerVojta Jina2012-01-09 13:17:48 -0800
commita13b5ed3bc337a493029815c595b89c39eb95af6 (patch)
tree2ca5380d5cf5aea68218280cccda5d0221517454 /src
parent63cca9afbcf7a772086eb4582d2f409c39e0ed12 (diff)
downloadangular.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.js57
-rw-r--r--src/service/http.js680
-rw-r--r--src/service/httpBackend.js40
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);
}
};
}
-