angular.js/src/service/http.js
2011-12-05 23:53:26 -08:00

511 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
/**
* Parse headers into key value object
*
* @param {string} headers Raw headers as a string
* @returns {Object} Parsed headers as key valu object
*/
function parseHeaders(headers) {
var parsed = {}, key, val, i;
forEach(headers.split('\n'), function(line) {
i = line.indexOf(':');
key = lowercase(trim(line.substr(0, i)));
val = trim(line.substr(i + 1));
if (key) {
if (parsed[key]) {
parsed[key] += ', ' + val;
} else {
parsed[key] = val;
}
}
});
return parsed;
}
/**
* Chain all given functions
*
* This function is used for both request and response transforming
*
* @param {*} data Data to transform.
* @param {function|Array.<function>} fns Function or an array of functions.
* @param {*=} param Optional parameter to be passed to all transform functions.
* @returns {*} Transformed data.
*/
function transform(data, fns, param) {
if (isFunction(fns))
return fns(data);
forEach(fns, function(fn) {
data = fn(data, param);
});
return data;
}
function $HttpProvider() {
var JSON_START = /^\s*(\[|\{[^\{])/,
JSON_END = /[\}\]]\s*$/,
PROTECTION_PREFIX = /^\)\]\}',?\n/;
var $config = this.defaults = {
// transform in-coming reponse data
transformResponse: function(data) {
if (isString(data)) {
// strip json vulnerability protection prefix
data = data.replace(PROTECTION_PREFIX, '');
if (JSON_START.test(data) && JSON_END.test(data))
data = fromJson(data, true);
}
return data;
},
// transform out-going request data
transformRequest: function(d) {
return isObject(d) ? toJson(d) : d;
},
// default headers
headers: {
common: {
'Accept': 'application/json, text/plain, */*',
'X-Requested-With': 'XMLHttpRequest'
},
post: {'Content-Type': 'application/json'},
put: {'Content-Type': 'application/json'}
}
};
var responseInterceptors = this.responseInterceptors = [];
this.$get = ['$httpBackend', '$browser', '$exceptionHandler', '$cacheFactory', '$rootScope', '$q', '$injector',
function($httpBackend, $browser, $exceptionHandler, $cacheFactory, $rootScope, $q, $injector) {
var defaultCache = $cacheFactory('$http');
forEach(responseInterceptors, function(interceptor, index) {
if (isString(interceptor)) {
responseInterceptors[index] = $injector.get(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);
});
promise.success = function(fn) {
promise.then(function(response) {
fn(response.data, response.status, response.headers, config);
});
return 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});
});
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
}));
};
});
}
function createShortMethodsWithData(name) {
forEach(arguments, function(name) {
$http[name] = function(url, data, config) {
return $http(extend(config || {}, {
method: name,
url: url,
data: data
}));
};
});
}
/**
* 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;
/**
* Callback registered to $httpBackend():
* - caches the response if desired
* - calls fireCallbacks()
* - clears the reference to raw request 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;
}
/**
* Fire all registered callbacks for given status code
*
* This method when:
* - serving response from real request
* - serving response from cache
*
* It does:
* - transform the response
* - call proper callbacks
* - log errors
* - apply the $scope
* - clear parsed headers
*/
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;
}
/**
* This is the third argument in any user callback
* @see parseHeaders
*
* Return single header value or all headers parsed as object.
* Headers all lazy parsed when first requested.
*
* @param {string=} name Name of header
* @returns {string|Object}
*/
function headers(name) {
if (name) {
return parsedHeaders ?
parsedHeaders[lowercase(name)] || null :
rawRequest.getResponseHeader(name);
}
parsedHeaders = parsedHeaders || parseHeaders(rawRequest.getAllResponseHeaders());
return parsedHeaders;
}
/**
* Retry the request
*
* @param {Object=} config Optional config object to extend the original configuration
* @returns {HttpPromise}
*/
this.retry = function(config) {
if (rawRequest) throw 'Can not retry request. Abort pending request first.';
extend(cfg, config);
cfg.method = uppercase(cfg.method);
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 (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,
// 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]);
});
} else {
// serving from cache - still needs to be async
$browser.defer(function() {
parsedHeaders = fromCache[2];
fireCallbacks(fromCache[1], fromCache[0]);
});
}
} else {
// put future object into cache
cache.put(cfg.url, self);
}
}
// really send the request
if (!cfg.cache || cfg.method !== 'GET' || !fromCache) {
rawRequest = $httpBackend(cfg.method, cfg.url, data, done, headers, cfg.timeout);
}
$http.pendingRequests.push(cfg);
return self;
};
// 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();
}
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, '.') + ')$')
});
return this;
};
/**
* Configuration object of the request
*/
this.config = cfg;
}
}];
}