Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 4ae4681

Browse files
inukshukjbdeboer
authored andcommitted
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'); });
1 parent 5c735eb commit 4ae4681

File tree

6 files changed

+480
-82
lines changed

6 files changed

+480
-82
lines changed

src/ng/compile.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -1018,10 +1018,10 @@ function $CompileProvider($provide) {
10181018

10191019

10201020
while(linkQueue.length) {
1021-
var controller = linkQueue.pop(),
1022-
linkRootElement = linkQueue.pop(),
1023-
beforeTemplateLinkNode = linkQueue.pop(),
1024-
scope = linkQueue.pop(),
1021+
var scope = linkQueue.shift(),
1022+
beforeTemplateLinkNode = linkQueue.shift(),
1023+
linkRootElement = linkQueue.shift(),
1024+
controller = linkQueue.shift(),
10251025
linkNode = compileNode;
10261026

10271027
if (beforeTemplateLinkNode !== beforeTemplateCompileNode) {

src/ng/http.js

+175-39
Original file line numberDiff line numberDiff line change
@@ -155,20 +155,52 @@ function $HttpProvider() {
155155
xsrfHeaderName: 'X-XSRF-TOKEN'
156156
};
157157

158-
var providerResponseInterceptors = this.responseInterceptors = [];
158+
/**
159+
* Are order by request. I.E. they are applied in the same order as
160+
* array on request, but revers order on response.
161+
*/
162+
var interceptorFactories = this.interceptors = [];
163+
/**
164+
* For historical reasons, response interceptors ordered by the order in which
165+
* they are applied to response. (This is in revers to interceptorFactories)
166+
*/
167+
var responseInterceptorFactories = this.responseInterceptors = [];
159168

160169
this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector',
161170
function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) {
162171

163-
var defaultCache = $cacheFactory('$http'),
164-
responseInterceptors = [];
172+
var defaultCache = $cacheFactory('$http');
165173

166-
forEach(providerResponseInterceptors, function(interceptor) {
167-
responseInterceptors.push(
168-
isString(interceptor)
169-
? $injector.get(interceptor)
170-
: $injector.invoke(interceptor)
171-
);
174+
/**
175+
* Interceptors stored in reverse order. Inner interceptors before outer interceptors.
176+
* The reversal is needed so that we can build up the interception chain around the
177+
* server request.
178+
*/
179+
var reversedInterceptors = [];
180+
181+
forEach(interceptorFactories, function(interceptorFactory) {
182+
reversedInterceptors.unshift(isString(interceptorFactory)
183+
? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory));
184+
});
185+
186+
forEach(responseInterceptorFactories, function(interceptorFactory, index) {
187+
var responseFn = isString(interceptorFactory)
188+
? $injector.get(interceptorFactory)
189+
: $injector.invoke(interceptorFactory);
190+
191+
/**
192+
* Response interceptors go before "around" interceptors (no real reason, just
193+
* had to pick one.) But they are already revesed, so we can't use unshift, hence
194+
* the splice.
195+
*/
196+
reversedInterceptors.splice(index, 0, {
197+
response: function(response) {
198+
return responseFn($q.when(response));
199+
},
200+
responseError: function(response) {
201+
return responseFn($q.reject(response));
202+
}
203+
});
172204
});
173205

174206

@@ -310,7 +342,90 @@ function $HttpProvider() {
310342
* To skip it, set configuration property `cache` to `false`.
311343
*
312344
*
313-
* # Response interceptors
345+
* # Interceptors
346+
*
347+
* Before you start creating interceptors, be sure to understand the
348+
* {@link ng.$q $q and deferred/promise APIs}.
349+
*
350+
* For purposes of global error handling, authentication or any kind of synchronous or
351+
* asynchronous pre-processing of request or postprocessing of responses, it is desirable to be
352+
* able to intercept requests before they are handed to the server and
353+
* responses before they are handed over to the application code that
354+
* initiated these requests. The interceptors leverage the {@link ng.$q
355+
* promise APIs} to fulfil this need for both synchronous and asynchronous pre-processing.
356+
*
357+
* The interceptors are service factories that are registered with the $httpProvider by
358+
* adding them to the `$httpProvider.interceptors` array. The factory is called and
359+
* injected with dependencies (if specified) and returns the interceptor.
360+
*
361+
* There are two kinds of interceptors (and two kinds of rejection interceptors):
362+
*
363+
* * `request`: interceptors get called with http `config` object. The function is free to modify
364+
* the `config` or create a new one. The function needs to return the `config` directly or as a
365+
* promise.
366+
* * `requestError`: interceptor gets called when a previous interceptor threw an error or resolved
367+
* with a rejection.
368+
* * `response`: interceptors get called with http `response` object. The function is free to modify
369+
* the `response` or create a new one. The function needs to return the `response` directly or as a
370+
* promise.
371+
* * `responseError`: interceptor gets called when a previous interceptor threw an error or resolved
372+
* with a rejection.
373+
*
374+
*
375+
* <pre>
376+
* // register the interceptor as a service
377+
* $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) {
378+
* return {
379+
* // optional method
380+
* 'request': function(config) {
381+
* // do something on success
382+
* return config || $q.when(config);
383+
* },
384+
*
385+
* // optional method
386+
* 'requestError': function(rejection) {
387+
* // do something on error
388+
* if (canRecover(rejection)) {
389+
* return responseOrNewPromise
390+
* }
391+
* return $q.reject(rejection);
392+
* },
393+
*
394+
*
395+
*
396+
* // optional method
397+
* 'response': function(response) {
398+
* // do something on success
399+
* return response || $q.when(response);
400+
* },
401+
*
402+
* // optional method
403+
* 'responseError': function(rejection) {
404+
* // do something on error
405+
* if (canRecover(rejection)) {
406+
* return responseOrNewPromise
407+
* }
408+
* return $q.reject(rejection);
409+
* };
410+
* }
411+
* });
412+
*
413+
* $httpProvider.interceptors.push('myHttpInterceptor');
414+
*
415+
*
416+
* // register the interceptor via an anonymous factory
417+
* $httpProvider.interceptors.push(function($q, dependency1, dependency2) {
418+
* return {
419+
* 'request': function(config) {
420+
* // same as above
421+
* },
422+
* 'response': function(response) {
423+
* // same as above
424+
* }
425+
* });
426+
* </pre>
427+
*
428+
* # Response interceptors (DEPRECATED)
314429
*
315430
* Before you start creating interceptors, be sure to understand the
316431
* {@link ng.$q $q and deferred/promise APIs}.
@@ -526,45 +641,66 @@ function $HttpProvider() {
526641
</file>
527642
</example>
528643
*/
529-
function $http(config) {
644+
function $http(requestConfig) {
645+
var config = {
646+
transformRequest: defaults.transformRequest,
647+
transformResponse: defaults.transformResponse
648+
};
649+
var headers = {};
650+
651+
extend(config, requestConfig);
652+
config.headers = headers;
530653
config.method = uppercase(config.method);
531654

532-
var xsrfHeader = {},
533-
xsrfCookieName = config.xsrfCookieName || defaults.xsrfCookieName,
534-
xsrfHeaderName = config.xsrfHeaderName || defaults.xsrfHeaderName,
535-
xsrfToken = isSameDomain(config.url, $browser.url()) ?
536-
$browser.cookies()[xsrfCookieName] : undefined;
537-
xsrfHeader[xsrfHeaderName] = xsrfToken;
538-
539-
var reqTransformFn = config.transformRequest || defaults.transformRequest,
540-
respTransformFn = config.transformResponse || defaults.transformResponse,
541-
defHeaders = defaults.headers,
542-
reqHeaders = extend(xsrfHeader,
543-
defHeaders.common, defHeaders[lowercase(config.method)], config.headers),
544-
reqData = transformData(config.data, headersGetter(reqHeaders), reqTransformFn),
545-
promise;
546-
547-
// strip content-type if data is undefined
548-
if (isUndefined(config.data)) {
549-
delete reqHeaders['Content-Type'];
550-
}
655+
extend(headers,
656+
defaults.headers.common,
657+
defaults.headers[lowercase(config.method)],
658+
requestConfig.headers);
551659

552-
if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) {
553-
config.withCredentials = defaults.withCredentials;
660+
var xsrfValue = isSameDomain(config.url, $browser.url())
661+
? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName]
662+
: undefined;
663+
if (xsrfValue) {
664+
headers[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue;
554665
}
555666

556-
// send request
557-
promise = sendReq(config, reqData, reqHeaders);
558667

668+
var serverRequest = function(config) {
669+
var reqData = transformData(config.data, headersGetter(headers), config.transformRequest);
559670

560-
// transform future response
561-
promise = promise.then(transformResponse, transformResponse);
671+
// strip content-type if data is undefined
672+
if (isUndefined(config.data)) {
673+
delete headers['Content-Type'];
674+
}
675+
676+
if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) {
677+
config.withCredentials = defaults.withCredentials;
678+
}
679+
680+
// send request
681+
return sendReq(config, reqData, headers).then(transformResponse, transformResponse);
682+
};
683+
684+
var chain = [serverRequest, undefined];
685+
var promise = $q.when(config);
562686

563687
// apply interceptors
564-
forEach(responseInterceptors, function(interceptor) {
565-
promise = interceptor(promise);
688+
forEach(reversedInterceptors, function(interceptor) {
689+
if (interceptor.request || interceptor.requestError) {
690+
chain.unshift(interceptor.request, interceptor.requestError);
691+
}
692+
if (interceptor.response || interceptor.responseError) {
693+
chain.push(interceptor.response, interceptor.responseError);
694+
}
566695
});
567696

697+
while(chain.length) {
698+
var thenFn = chain.shift();
699+
var rejectFn = chain.shift();
700+
701+
promise = promise.then(thenFn, rejectFn);
702+
};
703+
568704
promise.success = function(fn) {
569705
promise.then(function(response) {
570706
fn(response.data, response.status, response.headers, config);
@@ -584,7 +720,7 @@ function $HttpProvider() {
584720
function transformResponse(response) {
585721
// make a copy since the response must be cacheable
586722
var resp = extend({}, response, {
587-
data: transformData(response.data, response.headers, respTransformFn)
723+
data: transformData(response.data, response.headers, config.transformResponse)
588724
});
589725
return (isSuccess(response.status))
590726
? resp

src/ngMock/angular-mocks.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,7 @@ angular.mock.dump = function(object) {
826826
</pre>
827827
*/
828828
angular.mock.$HttpBackendProvider = function() {
829-
this.$get = [createHttpBackendMock];
829+
this.$get = ['$rootScope', createHttpBackendMock];
830830
};
831831

832832
/**
@@ -843,7 +843,7 @@ angular.mock.$HttpBackendProvider = function() {
843843
* @param {Object=} $browser Auto-flushing enabled if specified
844844
* @return {Object} Instance of $httpBackend mock
845845
*/
846-
function createHttpBackendMock($delegate, $browser) {
846+
function createHttpBackendMock($rootScope, $delegate, $browser) {
847847
var definitions = [],
848848
expectations = [],
849849
responses = [],
@@ -1173,6 +1173,7 @@ function createHttpBackendMock($delegate, $browser) {
11731173
* is called an exception is thrown (as this typically a sign of programming error).
11741174
*/
11751175
$httpBackend.flush = function(count) {
1176+
$rootScope.$digest();
11761177
if (!responses.length) throw Error('No pending request to flush !');
11771178

11781179
if (angular.isDefined(count)) {
@@ -1205,6 +1206,7 @@ function createHttpBackendMock($delegate, $browser) {
12051206
* </pre>
12061207
*/
12071208
$httpBackend.verifyNoOutstandingExpectation = function() {
1209+
$rootScope.$digest();
12081210
if (expectations.length) {
12091211
throw Error('Unsatisfied requests: ' + expectations.join(', '));
12101212
}
@@ -1606,7 +1608,7 @@ angular.module('ngMockE2E', ['ng']).config(function($provide) {
16061608
* control how a matched request is handled.
16071609
*/
16081610
angular.mock.e2e = {};
1609-
angular.mock.e2e.$httpBackendDecorator = ['$delegate', '$browser', createHttpBackendMock];
1611+
angular.mock.e2e.$httpBackendDecorator = ['$rootScope', '$delegate', '$browser', createHttpBackendMock];
16101612

16111613

16121614
angular.mock.clearDataCache = function() {

test/ng/directive/ngIncludeSpec.js

+5-7
Original file line numberDiff line numberDiff line change
@@ -178,25 +178,23 @@ describe('ngInclude', function() {
178178
it('should discard pending xhr callbacks if a new template is requested before the current ' +
179179
'finished loading', inject(function($rootScope, $compile, $httpBackend) {
180180
element = jqLite("<ng:include src='templateUrl'></ng:include>");
181-
var log = [];
181+
var log = {};
182182

183183
$rootScope.templateUrl = 'myUrl1';
184184
$rootScope.logger = function(msg) {
185-
log.push(msg);
185+
log[msg] = true;
186186
}
187187
$compile(element)($rootScope);
188-
expect(log.join('; ')).toEqual('');
188+
expect(log).toEqual({});
189189

190190
$httpBackend.expect('GET', 'myUrl1').respond('<div>{{logger("url1")}}</div>');
191191
$rootScope.$digest();
192-
expect(log.join('; ')).toEqual('');
192+
expect(log).toEqual({});
193193
$rootScope.templateUrl = 'myUrl2';
194194
$httpBackend.expect('GET', 'myUrl2').respond('<div>{{logger("url2")}}</div>');
195-
$rootScope.$digest();
196195
$httpBackend.flush(); // now that we have two requests pending, flush!
197196

198-
expect(log.join('; ')).toEqual('url2; url2'); // it's here twice because we go through at
199-
// least two digest cycles
197+
expect(log).toEqual({ url2 : true });
200198
}));
201199

202200

0 commit comments

Comments
 (0)