diff --git a/src/ng/http.js b/src/ng/http.js index 4288e7c17c1e..ab1ba9aa37a3 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -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); /** @@ -347,6 +341,40 @@ function $HttpProvider() { * * * + * # 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. + * + *
+     *   // 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;
+     *       });
+     *     };
+     *   });
+     * 
+ * + * * # Security Considerations * * When designing web applications, consider security threats from: @@ -520,6 +548,9 @@ function $HttpProvider() { */ function $http(config) { + // make a defensive copy of the config object + config = extend({}, config); + config.method = uppercase(config.method); var xsrfHeader = {}, @@ -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); }); @@ -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); @@ -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; @@ -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 = []; diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js index 600a6a2ebf8a..ce3198f1c977 100644 --- a/test/ng/httpSpec.js +++ b/test/ng/httpSpec.js @@ -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([]); @@ -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;