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

Commit

Permalink
feat(http): support request/response promise chaining
Browse files Browse the repository at this point in the history
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');
});
  • Loading branch information
inukshuk authored and jbdeboer committed Mar 27, 2013
1 parent 5c735eb commit 4ae4681
Show file tree
Hide file tree
Showing 6 changed files with 480 additions and 82 deletions.
8 changes: 4 additions & 4 deletions src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
214 changes: 175 additions & 39 deletions src/ng/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
});
});


Expand Down Expand Up @@ -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.
*
*
* <pre>
* // 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
* }
* });
* </pre>
*
* # Response interceptors (DEPRECATED)
*
* Before you start creating interceptors, be sure to understand the
* {@link ng.$q $q and deferred/promise APIs}.
Expand Down Expand Up @@ -526,45 +641,66 @@ function $HttpProvider() {
</file>
</example>
*/
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'];

This comment has been minimized.

Copy link
@AGmakonts

AGmakonts Dec 17, 2014

I know that this is old commit but we have stumbled upon an issue with content type in GET requests and I'm curious what's the reason for removing content type.

This comment has been minimized.

Copy link
@caitp

caitp Dec 17, 2014

Contributor

Content-Type doesn't make sense when there is no content

This comment has been minimized.

Copy link
@AGmakonts

AGmakonts Dec 17, 2014

But in case backend requires content type to determine version that should be used GET requests become useless

}

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);
Expand All @@ -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
Expand Down
8 changes: 5 additions & 3 deletions src/ngMock/angular-mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,7 @@ angular.mock.dump = function(object) {
</pre>
*/
angular.mock.$HttpBackendProvider = function() {
this.$get = [createHttpBackendMock];
this.$get = ['$rootScope', createHttpBackendMock];
};

/**
Expand All @@ -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 = [],
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -1205,6 +1206,7 @@ function createHttpBackendMock($delegate, $browser) {
* </pre>
*/
$httpBackend.verifyNoOutstandingExpectation = function() {
$rootScope.$digest();
if (expectations.length) {
throw Error('Unsatisfied requests: ' + expectations.join(', '));
}
Expand Down Expand Up @@ -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() {
Expand Down
12 changes: 5 additions & 7 deletions test/ng/directive/ngIncludeSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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("<ng:include src='templateUrl'></ng:include>");
var log = [];
var log = {};

$rootScope.templateUrl = 'myUrl1';
$rootScope.logger = function(msg) {
log.push(msg);
log[msg] = true;
}
$compile(element)($rootScope);
expect(log.join('; ')).toEqual('');
expect(log).toEqual({});

$httpBackend.expect('GET', 'myUrl1').respond('<div>{{logger("url1")}}</div>');
$rootScope.$digest();
expect(log.join('; ')).toEqual('');
expect(log).toEqual({});
$rootScope.templateUrl = 'myUrl2';
$httpBackend.expect('GET', 'myUrl2').respond('<div>{{logger("url2")}}</div>');
$rootScope.$digest();
$httpBackend.flush(); // now that we have two requests pending, flush!

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


Expand Down
Loading

0 comments on commit 4ae4681

Please sign in to comment.