angular.js/src/ng/http.js

766 lines
28 KiB
JavaScript
Raw Normal View History

'use strict';
/**
* Parse headers into key value object
*
* @param {string} headers Raw headers as a string
* @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)));
val = trim(line.substr(i + 1));
if (key) {
if (parsed[key]) {
parsed[key] += ', ' + val;
} else {
parsed[key] = val;
}
}
});
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(headers) {
var headersObj = isObject(headers) ? headers : undefined;
return function(name) {
if (!headersObj) headersObj = parseHeaders(headers);
if (name) {
return headersObj[lowercase(name)] || null;
}
return headersObj;
};
}
/**
* Chain all given functions
*
* This function is used for both request and response transforming
*
* @param {*} data Data to transform.
* @param {function(string=)} headers Http headers getter fn.
* @param {(function|Array.<function>)} fns Function or an array of functions.
* @returns {*} Transformed data.
*/
function transformData(data, headers, fns) {
if (isFunction(fns))
return fns(data, headers);
forEach(fns, function(fn) {
data = fn(data, headers);
});
return data;
}
function isSuccess(status) {
return 200 <= status && status < 300;
}
function $HttpProvider() {
var JSON_START = /^\s*(\[|\{[^\{])/,
2011-11-05 00:15:03 +00:00
JSON_END = /[\}\]]\s*$/,
PROTECTION_PREFIX = /^\)\]\}',?\n/;
var $config = this.defaults = {
// transform incoming response data
transformResponse: [function(data) {
if (isString(data)) {
// strip json vulnerability protection prefix
2011-11-05 00:15:03 +00:00
data = data.replace(PROTECTION_PREFIX, '');
if (JSON_START.test(data) && JSON_END.test(data))
data = fromJson(data, true);
}
return data;
}],
// transform outgoing request data
transformRequest: [function(d) {
return isObject(d) && !isFile(d) ? toJson(d) : d;
}],
// default headers
headers: {
common: {
'Accept': 'application/json, text/plain, */*',
'X-Requested-With': 'XMLHttpRequest'
},
post: {'Content-Type': 'application/json;charset=utf-8'},
put: {'Content-Type': 'application/json;charset=utf-8'}
}
};
var providerResponseInterceptors = this.responseInterceptors = [];
2011-11-30 08:58:34 +00:00
2012-01-07 03:17:31 +00:00
this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector',
function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) {
var defaultCache = $cacheFactory('$http'),
responseInterceptors = [];
forEach(providerResponseInterceptors, function(interceptor) {
responseInterceptors.push(
isString(interceptor)
? $injector.get(interceptor)
: $injector.invoke(interceptor)
);
});
/**
* @ngdoc function
* @name ng.$http
* @requires $httpBackend
* @requires $browser
* @requires $cacheFactory
2012-01-07 03:17:31 +00:00
* @requires $rootScope
* @requires $q
* @requires $injector
*
* @description
* The `$http` service is a core Angular service that facilitates communication with the remote
* HTTP servers via browser's {@link https://developer.mozilla.org/en/xmlhttprequest
* XMLHttpRequest} object or via {@link http://en.wikipedia.org/wiki/JSONP JSONP}.
*
* For unit testing applications that use `$http` service, see
* {@link ngMock.$httpBackend $httpBackend mock}.
*
* For a higher level of abstraction, please check out the {@link ngResource.$resource
* $resource} service.
*
* The $http API is based on the {@link ng.$q deferred/promise APIs} exposed by
* the $q service. While for simple usage patters this doesn't matter much, for advanced usage,
* it is important to familiarize yourself with these apis and guarantees they provide.
*
*
* # General usage
* The `$http` service is a function which takes a single argument a configuration object
* that is used to generate an http request and returns a {@link ng.$q promise}
* with two $http specific methods: `success` and `error`.
*
* <pre>
* $http({method: 'GET', url: '/someUrl'}).
* success(function(data, status, headers, config) {
* // this callback will be called asynchronously
* // when the response is available
* }).
* error(function(data, status, headers, config) {
* // called asynchronously if an error occurs
* // or server returns response with status
* // code outside of the <200, 400) range
* });
* </pre>
*
* Since the returned value of calling the $http function is a Promise object, you can also use
* the `then` method to register callbacks, and these callbacks will receive a single argument
* an object representing the response. See the api signature and type info below for more
* details.
*
*
* # Shortcut methods
*
* Since all invocation of the $http service require definition of the http method and url and
* POST and PUT requests require response body/data to be provided as well, shortcut methods
* were created to simplify using the api:
*
* <pre>
* $http.get('/someUrl').success(successCallback);
* $http.post('/someUrl', data).success(successCallback);
* </pre>
*
* Complete list of shortcut methods:
*
* - {@link ng.$http#get $http.get}
* - {@link ng.$http#head $http.head}
* - {@link ng.$http#post $http.post}
* - {@link ng.$http#put $http.put}
* - {@link ng.$http#delete $http.delete}
* - {@link ng.$http#jsonp $http.jsonp}
*
*
* # Setting HTTP Headers
*
* The $http service will automatically add certain http headers to all requests. These defaults
* can be fully configured by accessing the `$httpProvider.defaults.headers` configuration
* object, which currently contains this default configuration:
*
* - `$httpProvider.defaults.headers.common` (headers that are common for all requests):
* - `Accept: application/json, text/plain, * / *`
* - `X-Requested-With: XMLHttpRequest`
2012-01-22 00:32:30 +00:00
* - `$httpProvider.defaults.headers.post`: (header defaults for HTTP POST requests)
* - `Content-Type: application/json`
* - `$httpProvider.defaults.headers.put` (header defaults for HTTP PUT requests)
* - `Content-Type: application/json`
*
* To add or overwrite these defaults, simply add or remove a property from this configuration
* objects. To add headers for an HTTP method other than POST or PUT, simply add a new object
* with name equal to the lower-cased http method name, e.g.
* `$httpProvider.defaults.headers.get['My-Header']='value'`.
*
* Additionally, the defaults can be set at runtime via the `$http.defaults` object in a similar
* fassion as described above.
*
*
* # Transforming Requests and Responses
*
* Both requests and responses can be transformed using transform functions. By default, Angular
* applies these transformations:
*
* Request transformations:
*
* - if the `data` property of the request config object contains an object, serialize it into
* JSON format.
*
* Response transformations:
*
* - if XSRF prefix is detected, strip it (see Security Considerations section below)
* - if json response is detected, deserialize it using a JSON parser
*
* To override these transformation locally, specify transform functions as `transformRequest`
* and/or `transformResponse` properties of the config object. To globally override the default
* transforms, override the `$httpProvider.defaults.transformRequest` and
* `$httpProvider.defaults.transformResponse` properties of the `$httpProvider`.
*
*
* # Caching
*
* To enable caching set the configuration property `cache` to `true`. When the cache is
* enabled, `$http` stores the response from the server in local cache. Next time the
* response is served from the cache without sending a request to the server.
*
* Note that even if the response is served from cache, delivery of the data is asynchronous in
* the same way that real requests are.
*
* If there are multiple GET requests for the same url that should be cached using the same
* cache, but the cache is not populated yet, only one request to the server will be made and
* the remaining requests will be fulfilled using the response for the first request.
*
*
* # Response interceptors
*
* Before you start creating interceptors, be sure to understand the
* {@link ng.$q $q and deferred/promise APIs}.
*
* For purposes of global error handling, authentication or any kind of synchronous or
* asynchronous preprocessing of received responses, it is desirable to be able to intercept
* responses for http requests before they are handed over to the application code that
* initiated these requests. The response interceptors leverage the {@link ng.$q
* promise apis} to fulfil this need for both synchronous and asynchronous preprocessing.
*
* The interceptors are service factories that are registered with the $httpProvider by
* adding them to the `$httpProvider.responseInterceptors` array. The factory is called and
* injected with dependencies (if specified) and returns the interceptor a function that
* takes a {@link ng.$q promise} and returns the original or a new promise.
*
* <pre>
* // register the interceptor as a service
* $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) {
* return function(promise) {
* return promise.then(function(response) {
* // do something on success
* }, function(response) {
* // do something on error
* if (canRecover(response)) {
* return responseOrNewPromise
* }
* return $q.reject(response);
* });
* }
* });
*
* $httpProvider.responseInterceptors.push('myHttpInterceptor');
*
*
* // register the interceptor via an anonymous factory
* $httpProvider.responseInterceptors.push(function($q, dependency1, dependency2) {
* return function(promise) {
* // same as above
* }
* });
* </pre>
*
*
* # Security Considerations
*
* When designing web applications, consider security threats from:
*
* - {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx
* JSON Vulnerability}
* - {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF}
*
* Both server and the client must cooperate in order to eliminate these threats. Angular comes
* pre-configured with strategies that address these issues, but for this to work backend server
* cooperation is required.
*
* ## JSON Vulnerability Protection
*
* A {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx
* JSON Vulnerability} allows third party web-site to turn your JSON resource URL into
* {@link http://en.wikipedia.org/wiki/JSON#JSONP JSONP} request under some conditions. To
* counter this your server can prefix all JSON requests with following string `")]}',\n"`.
* Angular will automatically strip the prefix before processing it as JSON.
*
* For example if your server needs to return:
* <pre>
* ['one','two']
* </pre>
*
* which is vulnerable to attack, your server can return:
* <pre>
* )]}',
* ['one','two']
* </pre>
*
* Angular will strip the prefix, before processing the JSON.
*
*
* ## Cross Site Request Forgery (XSRF) Protection
*
* {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} is a technique by which
* an unauthorized site can gain your user's private data. Angular provides following mechanism
* to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie
* called `XSRF-TOKEN` and sets it as the HTTP header `X-XSRF-TOKEN`. Since only JavaScript that
* runs on your domain could read the cookie, your server can be assured that the XHR came from
* JavaScript running on your domain.
*
* To take advantage of this, your server needs to set a token in a JavaScript readable session
* cookie called `XSRF-TOKEN` on first HTTP GET request. On subsequent non-GET requests the
* server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure
* that only JavaScript running on your domain could have read the token. The token must be
* unique for each user and must be verifiable by the server (to prevent the JavaScript making
* up its own tokens). We recommend that the token is a digest of your site's authentication
* cookie with {@link http://en.wikipedia.org/wiki/Rainbow_table salt for added security}.
*
*
* @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.
* - **params** `{Object.<string|Object>}` Map of strings or objects which will be turned to
* `?key1=value1&key2=value2` after the url. If the value is not a string, it will be JSONified.
* - **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.
* - **transformRequest** `{function(data, headersGetter)|Array.<function(data, headersGetter)>}`
* transform function or an array of such functions. The transform function takes the http
* request body and headers and returns its transformed (typically serialized) version.
* - **transformResponse** `{function(data, headersGetter)|Array.<function(data, headersGetter)>}`
* transform function or an array of such functions. The transform function takes the http
* response body and headers and returns its transformed (typically deserialized) version.
* - **cache** `{boolean|Cache}` If true, a default $http cache will be used to cache the
* GET request, otherwise if a cache instance built with
* {@link ng.$cacheFactory $cacheFactory}, this cache will be used for
* caching.
* - **timeout** `{number}` timeout in milliseconds.
* - **withCredentials** - `{boolean}` - whether to to set the `withCredentials` flag on the
* XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5
* requests with credentials} for more information.
*
* @returns {HttpPromise} Returns a {@link ng.$q 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 currently pending
* requests. This is primarily meant to be used for debugging purposes.
*
*
* @example
<example>
<file name="index.html">
<div ng-controller="FetchCtrl">
<select ng-model="method">
<option>GET</option>
<option>JSONP</option>
</select>
<input type="text" ng-model="url" size="80"/>
<button ng-click="fetch()">fetch</button><br>
<button ng-click="updateModel('GET', 'http-hello.html')">Sample GET</button>
<button ng-click="updateModel('JSONP', 'http://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')">Sample JSONP</button>
<button ng-click="updateModel('JSONP', 'http://angularjs.org/doesntexist&callback=JSON_CALLBACK')">Invalid JSONP</button>
<pre>http status code: {{status}}</pre>
<pre>http response data: {{data}}</pre>
</div>
</file>
<file name="script.js">
function FetchCtrl($scope, $http, $templateCache) {
$scope.method = 'GET';
$scope.url = 'http-hello.html';
$scope.fetch = function() {
$scope.code = null;
$scope.response = null;
$http({method: $scope.method, url: $scope.url, cache: $templateCache}).
success(function(data, status) {
$scope.status = status;
$scope.data = data;
}).
error(function(data, status) {
$scope.data = data || "Request failed";
$scope.status = status;
});
};
$scope.updateModel = function(method, url) {
$scope.method = method;
$scope.url = url;
};
}
</file>
<file name="http-hello.html">
Hello, $http!
</file>
<file name="scenario.js">
it('should make an xhr GET request', function() {
element(':button:contains("Sample GET")').click();
element(':button:contains("fetch")').click();
expect(binding('status')).toBe('200');
expect(binding('data')).toMatch(/Hello, \$http!/);
});
it('should make a JSONP request to angularjs.org', function() {
element(':button:contains("Sample JSONP")').click();
element(':button:contains("fetch")').click();
expect(binding('status')).toBe('200');
expect(binding('data')).toMatch(/Super Hero!/);
});
it('should make JSONP request to invalid URL and invoke the error handler',
function() {
element(':button:contains("Invalid JSONP")').click();
element(':button:contains("fetch")').click();
expect(binding('status')).toBe('0');
expect(binding('data')).toBe('Request failed');
});
</file>
</example>
*/
function $http(config) {
config.method = uppercase(config.method);
2011-11-30 08:58:34 +00:00
var reqTransformFn = config.transformRequest || $config.transformRequest,
respTransformFn = config.transformResponse || $config.transformResponse,
defHeaders = $config.headers,
reqHeaders = extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']},
defHeaders.common, defHeaders[lowercase(config.method)], config.headers),
reqData = transformData(config.data, headersGetter(reqHeaders), reqTransformFn),
promise;
// strip content-type if data is undefined
if (isUndefined(config.data)) {
delete reqHeaders['Content-Type'];
}
// send request
promise = sendReq(config, reqData, reqHeaders);
// 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;
};
promise.error = function(fn) {
promise.then(null, function(response) {
fn(response.data, response.status, response.headers, config);
});
return promise;
};
return promise;
function transformResponse(response) {
// make a copy since the response must be cacheable
var resp = extend({}, response, {
data: transformData(response.data, response.headers, respTransformFn)
});
return (isSuccess(response.status))
? resp
: $q.reject(resp);
}
}
$http.pendingRequests = [];
/**
* @ngdoc method
* @name ng.$http#get
* @methodOf 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 ng.$http#delete
* @methodOf 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 ng.$http#head
* @methodOf 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
2012-04-11 06:30:22 +00:00
* @returns {HttpPromise} Future object
*/
/**
* @ngdoc method
* @name ng.$http#jsonp
* @methodOf 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
2012-04-11 06:30:22 +00:00
* @returns {HttpPromise} Future object
*/
createShortMethods('get', 'delete', 'head', 'jsonp');
/**
* @ngdoc method
* @name ng.$http#post
* @methodOf 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 ng.$http#put
* @methodOf 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
2012-04-11 06:30:22 +00:00
* @returns {HttpPromise} Future object
*/
createShortMethodsWithData('post', 'put');
/**
* @ngdoc property
* @name ng.$http#defaults
* @propertyOf ng.$http
*
* @description
* Runtime equivalent of the `$httpProvider.defaults` property. Allows configuration of
* default headers as well as request and response transformations.
*
* See "Setting HTTP Headers" and "Transforming Requests and Responses" sections above.
*/
$http.defaults = $config;
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
}));
};
});
}
/**
* Makes the request
*
* !!! ACCESSES CLOSURE VARS:
* $httpBackend, $config, $log, $rootScope, defaultCache, $http.pendingRequests
*/
function sendReq(config, reqData, reqHeaders) {
var deferred = $q.defer(),
promise = deferred.promise,
cache,
cachedResp,
url = buildUrl(config.url, config.params);
$http.pendingRequests.push(config);
promise.then(removePendingReq, removePendingReq);
if (config.cache && config.method == 'GET') {
cache = isObject(config.cache) ? config.cache : defaultCache;
}
if (cache) {
cachedResp = cache.get(url);
if (cachedResp) {
if (cachedResp.then) {
2012-01-07 03:17:31 +00:00
// cached request has already been sent, but there is no response yet
cachedResp.then(removePendingReq, removePendingReq);
return cachedResp;
} else {
// serving from cache
if (isArray(cachedResp)) {
resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2]));
} else {
resolvePromise(cachedResp, 200, {});
}
}
} else {
// put the promise for the non-transformed response into cache as a placeholder
cache.put(url, promise);
}
}
// if we won't have the response in cache, send the request to the backend
if (!cachedResp) {
$httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout,
config.withCredentials);
}
return promise;
/**
* 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(url, [status, response, parseHeaders(headersString)]);
} else {
// remove promise from the cache
cache.remove(url);
}
}
resolvePromise(response, status, headersString);
$rootScope.$apply();
}
/**
* Resolves the raw $http promise.
*/
function resolvePromise(response, status, headers) {
// normalize internal statuses to 0
status = Math.max(status, 0);
(isSuccess(status) ? deferred.resolve : deferred.reject)({
data: response,
status: status,
headers: headersGetter(headers),
config: config
});
}
function removePendingReq() {
var idx = indexOf($http.pendingRequests, config);
if (idx !== -1) $http.pendingRequests.splice(idx, 1);
}
}
function buildUrl(url, params) {
if (!params) return url;
var parts = [];
forEachSorted(params, function(value, key) {
if (value == null || value == undefined) return;
if (isObject(value)) {
value = toJson(value);
}
parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
});
return url + ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&');
}
}];
}