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

feat($http): promise-based request interceptors #1701

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 92 additions & 22 deletions src/ng/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,21 +155,15 @@ function $HttpProvider() {
xsrfHeaderName: 'X-XSRF-TOKEN'
};

var providerResponseInterceptors = this.responseInterceptors = [];
var providerResponseInterceptors = this.responseInterceptors = [],
providerRequestInterceptors = this.requestInterceptors = [];

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)
);
});
requestInterceptors = resolveInterceptors(providerRequestInterceptors),
responseInterceptors = resolveInterceptors(providerResponseInterceptors);


/**
Expand Down Expand Up @@ -347,6 +341,40 @@ function $HttpProvider() {
* </pre>
*
*
* # Request interceptors
*
* For purposes of global request preprocessing Angular provides request
* interceptors. Request interceptors can be registered analogously to
* response interceptors by adding a service factory to the
* `$httpProvider.requestInterceptors` array. The factory is called and
* injected with dependencies (if specified) and returns the interceptor
* function: this function will be called for every HTTP request and
* passed a promise which will be resolved with the request's `config`
* object.
*
* <pre>
* // register the interceptor via an anonymous factory
* $httpProvider.requestInterceptors.push(function(dependency1, dependency2) {
* return function(promise) {
* return promise.then(function(config) {
* // do something with the config object
*
* // example: alter the request's URL
* config.url = '/intercepted';
*
* // example: add an extra HTTP header
* config.headers.custom = 'intercepted';
*
* // example: modify the request's data
* config.data = { intercepted: true };
*
* return config;
* });
* };
* });
* </pre>
*
*
* # Security Considerations
*
* When designing web applications, consider security threats from:
Expand Down Expand Up @@ -520,6 +548,9 @@ function $HttpProvider() {
</example>
*/
function $http(config) {
// make a defensive copy of the config object
config = extend({}, config);

config.method = uppercase(config.method);

var xsrfHeader = {},
Expand All @@ -532,28 +563,49 @@ function $HttpProvider() {
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;
deferred, promise;

config.headers = extend(xsrfHeader, defHeaders.common,
defHeaders[lowercase(config.method)], config.headers);

config.data = transformData(config.data, headersGetter(config.headers), reqTransformFn);

// strip content-type if data is undefined
if (isUndefined(config.data)) {
delete reqHeaders['Content-Type'];
delete config.headers['Content-Type'];
}

if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) {
config.withCredentials = defaults.withCredentials;
}

// send request
promise = sendReq(config, reqData, reqHeaders);

// apply request interceptors
if (requestInterceptors.length) {
deferred = $q.defer();
promise = deferred.promise;

forEach(requestInterceptors, function(interceptor) {
promise = interceptor(promise);
});

// send request after interceptors have finished
promise = promise.then(function(config) {
return sendReq(config);
});

deferred.resolve(config);

} else {
// if there are no request interceptors just send request
promise = sendReq(config);
}


// transform future response
promise = promise.then(transformResponse, transformResponse);

// apply interceptors
// apply response interceptors
forEach(responseInterceptors, function(interceptor) {
promise = interceptor(promise);
});
Expand Down Expand Up @@ -718,12 +770,20 @@ function $HttpProvider() {
* !!! ACCESSES CLOSURE VARS:
* $httpBackend, defaults, $log, $rootScope, defaultCache, $http.pendingRequests
*/
function sendReq(config, reqData, reqHeaders) {
function sendReq(config) {
var deferred = $q.defer(),
promise = deferred.promise,
cache,
cachedResp,
url = buildUrl(config.url, config.params);
url = buildUrl(config.url, config.params),
reqHeaders = config.headers,
reqData = config.data;

// remove data/headers from config object, because ngResource
// tests which do not expect these in the config object after
// the request
delete config.data;
delete config.headers;

$http.pendingRequests.push(config);
promise.then(removePendingReq, removePendingReq);
Expand Down Expand Up @@ -756,8 +816,8 @@ function $HttpProvider() {

// 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, config.responseType);
$httpBackend(config.method, url, reqData, done, reqHeaders,
config.timeout, config.withCredentials, config.responseType);
}

return promise;
Expand Down Expand Up @@ -807,6 +867,16 @@ function $HttpProvider() {
}


function resolveInterceptors(interceptors) {
var list = [];
forEach(interceptors, function(interceptor) {
list.push(
$injector[isString(interceptor) ? 'get' : 'invoke'](interceptor)
);
});
return list;
};

function buildUrl(url, params) {
if (!params) return url;
var parts = [];
Expand Down
138 changes: 137 additions & 1 deletion test/ng/httpSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('$http', function() {

describe('$httpProvider', function() {

describe('interceptors', function() {
describe('response interceptors', function() {

it('should default to an empty array', module(function($httpProvider) {
expect($httpProvider.responseInterceptors).toEqual([]);
Expand Down Expand Up @@ -103,6 +103,142 @@ describe('$http', function() {
});


describe('request interceptors', function() {
it('are not defined by default', module(function($httpProvider) {
expect($httpProvider.requestInterceptors).toEqual([]);
}));

it('are passed a the request config as a promise', function() {
module(function($httpProvider) {
$httpProvider.requestInterceptors.push(function($rootScope) {
return function(promise) {
return promise.then(function(config) {
expect(config.url).toBe('/url');
expect(config.data).toBe('{"one":"two"}');
expect(config.headers.foo).toBe('bar');
return config;
});
};
});
});
inject(function($http, $httpBackend, $rootScope) {
$httpBackend.expect('POST', '/url').respond('');
$http({method: 'POST', url: '/url', data: {one: 'two'}, headers: {foo: 'bar'}});
$rootScope.$apply();
});
});

it('can manipulate the request', function() {
module(function($httpProvider) {
$httpProvider.requestInterceptors.push(function() {
return function(promise) {
return promise.then(function(config) {
config.url = '/intercepted';
config.headers.foo = 'intercepted';
return config;
});
};
});
});
inject(function($http, $httpBackend, $rootScope) {
$httpBackend.expect('GET', '/intercepted', null, function (headers) {
return headers.foo === 'intercepted';
}).respond('');
$http.get('/url');
$rootScope.$apply();
});
});

it('rejects the http promise if an interceptor fails', function() {
var reason = new Error('interceptor failed');
module(function($httpProvider) {
$httpProvider.requestInterceptors.push(function($q) {
return function(promise) {
return $q.reject(reason);
};
});
});
inject(function($http, $httpBackend, $rootScope) {
var success = jasmine.createSpy(), error = jasmine.createSpy();
$http.get('/url').then(success, error);
$rootScope.$apply();
expect(success).not.toHaveBeenCalled();
expect(error).toHaveBeenCalledWith(reason);
$httpBackend.verifyNoOutstandingRequest();
});
});

it('does not manipulate the passed-in config', function() {
module(function($httpProvider) {
$httpProvider.requestInterceptors.push(function() {
return function(promise) {
return promise.then(function(config) {
config.url = '/intercepted';
config.headers.foo = 'intercepted';
return config;
});
};
});
});
inject(function($http, $httpBackend, $rootScope) {
var config = { method: 'get', url: '/url', headers: { foo: 'bar'} };
$httpBackend.expect('GET', '/intercepted').respond('');
$http.get('/url');
$rootScope.$apply();
expect(config.method).toBe('get');
expect(config.url).toBe('/url');
expect(config.headers.foo).toBe('bar')
});
});

it('supports interceptors defined as services', function() {
module(function($provide, $httpProvider) {
$provide.factory('myInterceptor', function() {
return function(promise) {
return promise.then(function(config) {
config.url = '/intercepted';
return config;
});
};
});
$httpProvider.requestInterceptors.push('myInterceptor');
});
inject(function($http, $httpBackend, $rootScope) {
$httpBackend.expect('POST', '/intercepted').respond('');
$http.post('/url');
$rootScope.$apply();
});
});

it('supports complex interceptors based on promises', function() {
module(function($provide, $httpProvider) {
$provide.factory('myInterceptor', function($q, $rootScope) {
return function(promise) {
var deferred = $q.defer();

$rootScope.$apply(function() {
deferred.resolve('/intercepted');
});

return deferred.promise.then(function(intercepted) {
return promise.then(function(config) {
config.url = intercepted;
return config;
});
});
};
});
$httpProvider.requestInterceptors.push('myInterceptor');
});
inject(function($http, $httpBackend, $rootScope) {
$httpBackend.expect('POST', '/intercepted').respond('');
$http.post('/two');
$rootScope.$apply();
});
});
});


describe('the instance', function() {
var $httpBackend, $http, $rootScope;

Expand Down