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/compile.js | 8 +- src/ng/http.js | 214 ++++++++++++++++++++++----- src/ngMock/angular-mocks.js | 8 +- test/ng/directive/ngIncludeSpec.js | 12 +- test/ng/httpSpec.js | 288 ++++++++++++++++++++++++++++++++++++- test/ngResource/resourceSpec.js | 32 ++--- 6 files changed, 480 insertions(+), 82 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 6606dc6c..68a3dca1 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1018,10 +1018,10 @@ function $CompileProvider($provide) { while(linkQueue.length) { - var controller = linkQueue.pop(), - linkRootElement = linkQueue.pop(), - beforeTemplateLinkNode = linkQueue.pop(), - scope = linkQueue.pop(), + var scope = linkQueue.shift(), + beforeTemplateLinkNode = linkQueue.shift(), + linkRootElement = linkQueue.shift(), + controller = linkQueue.shift(), linkNode = compileNode; if (beforeTemplateLinkNode !== beforeTemplateCompileNode) { 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
diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js
index 8c91e628..3980a391 100644
--- a/src/ngMock/angular-mocks.js
+++ b/src/ngMock/angular-mocks.js
@@ -826,7 +826,7 @@ angular.mock.dump = function(object) {
*/
angular.mock.$HttpBackendProvider = function() {
- this.$get = [createHttpBackendMock];
+ this.$get = ['$rootScope', createHttpBackendMock];
};
/**
@@ -843,7 +843,7 @@ angular.mock.$HttpBackendProvider = function() {
* @param {Object=} $browser Auto-flushing enabled if specified
* @return {Object} Instance of $httpBackend mock
*/
-function createHttpBackendMock($delegate, $browser) {
+function createHttpBackendMock($rootScope, $delegate, $browser) {
var definitions = [],
expectations = [],
responses = [],
@@ -1173,6 +1173,7 @@ function createHttpBackendMock($delegate, $browser) {
* is called an exception is thrown (as this typically a sign of programming error).
*/
$httpBackend.flush = function(count) {
+ $rootScope.$digest();
if (!responses.length) throw Error('No pending request to flush !');
if (angular.isDefined(count)) {
@@ -1205,6 +1206,7 @@ function createHttpBackendMock($delegate, $browser) {
*
*/
$httpBackend.verifyNoOutstandingExpectation = function() {
+ $rootScope.$digest();
if (expectations.length) {
throw Error('Unsatisfied requests: ' + expectations.join(', '));
}
@@ -1606,7 +1608,7 @@ angular.module('ngMockE2E', ['ng']).config(function($provide) {
* control how a matched request is handled.
*/
angular.mock.e2e = {};
-angular.mock.e2e.$httpBackendDecorator = ['$delegate', '$browser', createHttpBackendMock];
+angular.mock.e2e.$httpBackendDecorator = ['$rootScope', '$delegate', '$browser', createHttpBackendMock];
angular.mock.clearDataCache = function() {
diff --git a/test/ng/directive/ngIncludeSpec.js b/test/ng/directive/ngIncludeSpec.js
index 7c94a70e..dce803b5 100644
--- a/test/ng/directive/ngIncludeSpec.js
+++ b/test/ng/directive/ngIncludeSpec.js
@@ -178,25 +178,23 @@ describe('ngInclude', function() {
it('should discard pending xhr callbacks if a new template is requested before the current ' +
'finished loading', inject(function($rootScope, $compile, $httpBackend) {
element = jqLite("