From 4ae46814ff4e7c0bbcdbbefc0a97277283a84065 Mon Sep 17 00:00:00 2001 From: Sylvester Keil Date: Tue, 26 Feb 2013 10:22:12 +0100 Subject: feat(http): support request/response promise chaining myApp.factory('myAroundInterceptor', function($rootScope, $timeout) { return function(configPromise, responsePromise) { return { request: configPromise.then(function(config) { return config }); response: responsePromise.then(function(response) { return 'ha!'; } }); } myApp.config(function($httpProvider){ $httpProvider.aroundInterceptors.push('myAroundInterceptor'); }); --- src/ng/http.js | 214 ++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 175 insertions(+), 39 deletions(-) (limited to 'src/ng/http.js') diff --git a/src/ng/http.js b/src/ng/http.js index e4d695c4..d54e8bd3 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -155,20 +155,52 @@ function $HttpProvider() { xsrfHeaderName: 'X-XSRF-TOKEN' }; - var providerResponseInterceptors = this.responseInterceptors = []; + /** + * Are order by request. I.E. they are applied in the same order as + * array on request, but revers order on response. + */ + var interceptorFactories = this.interceptors = []; + /** + * For historical reasons, response interceptors ordered by the order in which + * they are applied to response. (This is in revers to interceptorFactories) + */ + var responseInterceptorFactories = this.responseInterceptors = []; this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { - var defaultCache = $cacheFactory('$http'), - responseInterceptors = []; + var defaultCache = $cacheFactory('$http'); - forEach(providerResponseInterceptors, function(interceptor) { - responseInterceptors.push( - isString(interceptor) - ? $injector.get(interceptor) - : $injector.invoke(interceptor) - ); + /** + * Interceptors stored in reverse order. Inner interceptors before outer interceptors. + * The reversal is needed so that we can build up the interception chain around the + * server request. + */ + var reversedInterceptors = []; + + forEach(interceptorFactories, function(interceptorFactory) { + reversedInterceptors.unshift(isString(interceptorFactory) + ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory)); + }); + + forEach(responseInterceptorFactories, function(interceptorFactory, index) { + var responseFn = isString(interceptorFactory) + ? $injector.get(interceptorFactory) + : $injector.invoke(interceptorFactory); + + /** + * Response interceptors go before "around" interceptors (no real reason, just + * had to pick one.) But they are already revesed, so we can't use unshift, hence + * the splice. + */ + reversedInterceptors.splice(index, 0, { + response: function(response) { + return responseFn($q.when(response)); + }, + responseError: function(response) { + return responseFn($q.reject(response)); + } + }); }); @@ -310,7 +342,90 @@ function $HttpProvider() { * To skip it, set configuration property `cache` to `false`. * * - * # Response interceptors + * # 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 pre-processing of request or postprocessing of responses, it is desirable to be + * able to intercept requests before they are handed to the server and + * responses before they are handed over to the application code that + * initiated these requests. The interceptors leverage the {@link ng.$q + * promise APIs} to fulfil this need for both synchronous and asynchronous pre-processing. + * + * The interceptors are service factories that are registered with the $httpProvider by + * adding them to the `$httpProvider.interceptors` array. The factory is called and + * injected with dependencies (if specified) and returns the interceptor. + * + * There are two kinds of interceptors (and two kinds of rejection interceptors): + * + * * `request`: interceptors get called with http `config` object. The function is free to modify + * the `config` or create a new one. The function needs to return the `config` directly or as a + * promise. + * * `requestError`: interceptor gets called when a previous interceptor threw an error or resolved + * with a rejection. + * * `response`: interceptors get called with http `response` object. The function is free to modify + * the `response` or create a new one. The function needs to return the `response` directly or as a + * promise. + * * `responseError`: interceptor gets called when a previous interceptor threw an error or resolved + * with a rejection. + * + * + *
+ * // register the interceptor as a service
+ * $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) {
+ * return {
+ * // optional method
+ * 'request': function(config) {
+ * // do something on success
+ * return config || $q.when(config);
+ * },
+ *
+ * // optional method
+ * 'requestError': function(rejection) {
+ * // do something on error
+ * if (canRecover(rejection)) {
+ * return responseOrNewPromise
+ * }
+ * return $q.reject(rejection);
+ * },
+ *
+ *
+ *
+ * // optional method
+ * 'response': function(response) {
+ * // do something on success
+ * return response || $q.when(response);
+ * },
+ *
+ * // optional method
+ * 'responseError': function(rejection) {
+ * // do something on error
+ * if (canRecover(rejection)) {
+ * return responseOrNewPromise
+ * }
+ * return $q.reject(rejection);
+ * };
+ * }
+ * });
+ *
+ * $httpProvider.interceptors.push('myHttpInterceptor');
+ *
+ *
+ * // register the interceptor via an anonymous factory
+ * $httpProvider.interceptors.push(function($q, dependency1, dependency2) {
+ * return {
+ * 'request': function(config) {
+ * // same as above
+ * },
+ * 'response': function(response) {
+ * // same as above
+ * }
+ * });
+ *
+ *
+ * # Response interceptors (DEPRECATED)
*
* Before you start creating interceptors, be sure to understand the
* {@link ng.$q $q and deferred/promise APIs}.
@@ -526,45 +641,66 @@ function $HttpProvider() {
*/
- function $http(config) {
+ function $http(requestConfig) {
+ var config = {
+ transformRequest: defaults.transformRequest,
+ transformResponse: defaults.transformResponse
+ };
+ var headers = {};
+
+ extend(config, requestConfig);
+ config.headers = headers;
config.method = uppercase(config.method);
- var xsrfHeader = {},
- xsrfCookieName = config.xsrfCookieName || defaults.xsrfCookieName,
- xsrfHeaderName = config.xsrfHeaderName || defaults.xsrfHeaderName,
- xsrfToken = isSameDomain(config.url, $browser.url()) ?
- $browser.cookies()[xsrfCookieName] : undefined;
- xsrfHeader[xsrfHeaderName] = xsrfToken;
-
- var reqTransformFn = config.transformRequest || defaults.transformRequest,
- respTransformFn = config.transformResponse || defaults.transformResponse,
- defHeaders = defaults.headers,
- reqHeaders = extend(xsrfHeader,
- 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'];
- }
+ extend(headers,
+ defaults.headers.common,
+ defaults.headers[lowercase(config.method)],
+ requestConfig.headers);
- if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) {
- config.withCredentials = defaults.withCredentials;
+ var xsrfValue = isSameDomain(config.url, $browser.url())
+ ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName]
+ : undefined;
+ if (xsrfValue) {
+ headers[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue;
}
- // send request
- promise = sendReq(config, reqData, reqHeaders);
+ var serverRequest = function(config) {
+ var reqData = transformData(config.data, headersGetter(headers), config.transformRequest);
- // transform future response
- promise = promise.then(transformResponse, transformResponse);
+ // strip content-type if data is undefined
+ if (isUndefined(config.data)) {
+ delete headers['Content-Type'];
+ }
+
+ if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) {
+ config.withCredentials = defaults.withCredentials;
+ }
+
+ // send request
+ return sendReq(config, reqData, headers).then(transformResponse, transformResponse);
+ };
+
+ var chain = [serverRequest, undefined];
+ var promise = $q.when(config);
// apply interceptors
- forEach(responseInterceptors, function(interceptor) {
- promise = interceptor(promise);
+ forEach(reversedInterceptors, function(interceptor) {
+ if (interceptor.request || interceptor.requestError) {
+ chain.unshift(interceptor.request, interceptor.requestError);
+ }
+ if (interceptor.response || interceptor.responseError) {
+ chain.push(interceptor.response, interceptor.responseError);
+ }
});
+ while(chain.length) {
+ var thenFn = chain.shift();
+ var rejectFn = chain.shift();
+
+ promise = promise.then(thenFn, rejectFn);
+ };
+
promise.success = function(fn) {
promise.then(function(response) {
fn(response.data, response.status, response.headers, config);
@@ -584,7 +720,7 @@ function $HttpProvider() {
function transformResponse(response) {
// make a copy since the response must be cacheable
var resp = extend({}, response, {
- data: transformData(response.data, response.headers, respTransformFn)
+ data: transformData(response.data, response.headers, config.transformResponse)
});
return (isSuccess(response.status))
? resp
--
cgit v1.2.3