diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 814a12519799..9a7b182f669b 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -67,6 +67,8 @@ $IntervalProvider, $$HashMapProvider, $HttpProvider, + $HttpParamSerializerProvider, + $HttpParamSerializerJQLikeProvider, $HttpBackendProvider, $LocationProvider, $LogProvider, @@ -224,6 +226,8 @@ function publishExternalAPI(angular) { $interpolate: $InterpolateProvider, $interval: $IntervalProvider, $http: $HttpProvider, + $httpParamSerializer: $HttpParamSerializerProvider, + $httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider, $httpBackend: $HttpBackendProvider, $location: $LocationProvider, $log: $LogProvider, diff --git a/src/ng/http.js b/src/ng/http.js index 83f596286c49..0e56afad8586 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -9,6 +9,64 @@ var JSON_ENDS = { }; var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/; +function paramSerializerFactory(jQueryMode) { + + function serializeValue(v) { + if (isObject(v)) { + return isDate(v) ? v.toISOString() : toJson(v); + } + return v; + } + + return function paramSerializer(params) { + if (!params) return ''; + var parts = []; + forEachSorted(params, function(value, key) { + if (value === null || isUndefined(value)) return; + if (isArray(value) || isObject(value) && jQueryMode) { + forEach(value, function(v, k) { + var keySuffix = jQueryMode ? '[' + (!isArray(value) ? k : '') + ']' : ''; + parts.push(encodeUriQuery(key + keySuffix) + '=' + encodeUriQuery(serializeValue(v))); + }); + } else { + parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(value))); + } + }); + + return parts.length > 0 ? parts.join('&') : ''; + }; +} + +function $HttpParamSerializerProvider() { + /** + * @ngdoc service + * @name $httpParamSerializer + * @description + * + * Default $http params serializer that converts objects to a part of a request URL + * according to the following rules: + * * `{'foo': 'bar'}` results in `foo=bar` + * * `{'foo': Date.now()}` results in `foo=2015-04-01T09%3A50%3A49.262Z` (`toISOString()` and encoded representation of a Date object) + * * `{'foo': ['bar', 'baz']}` results in `foo=bar&foo=baz` (repeated key for each array element) + * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D"` (stringified and encoded representation of an object) + * */ + this.$get = function() { + return paramSerializerFactory(false); + }; +} + +function $HttpParamSerializerJQLikeProvider() { + /** + * @ngdoc service + * @name $httpParamSerializerJQLike + * + * Alternative $http params serializer that follows jQuerys `param()` method {http://api.jquery.com/jquery.param/} logic. + * */ + this.$get = function() { + return paramSerializerFactory(true); + }; +} + function defaultHttpResponseTransform(data, headers) { if (isString(data)) { // Strip json vulnerability protection prefix and trim whitespace @@ -153,6 +211,11 @@ function $HttpProvider() { * - **`defaults.headers.put`** * - **`defaults.headers.patch`** * + * - **`defaults.paramSerializer`** - {string|function(Object):string} - A function used to prepare string representation + * of request parameters (specified as an object). + * Is specified as string, it is interpreted as function registered in with the {$injector}. + * Defaults to {$httpParamSerializer}. + * **/ var defaults = this.defaults = { // transform incoming response data @@ -174,7 +237,9 @@ function $HttpProvider() { }, xsrfCookieName: 'XSRF-TOKEN', - xsrfHeaderName: 'X-XSRF-TOKEN' + xsrfHeaderName: 'X-XSRF-TOKEN', + + paramSerializer: '$httpParamSerializer' }; var useApplyAsync = false; @@ -188,7 +253,7 @@ function $HttpProvider() { * significant performance improvement for bigger applications that make many HTTP requests * concurrently (common during application bootstrap). * - * Defaults to false. If no value is specifed, returns the current configured value. + * Defaults to false. If no value is specified, returns the current configured value. * * @param {boolean=} value If true, when requests are loaded, they will schedule a deferred * "apply" on the next tick, giving time for subsequent requests in a roughly ~10ms window @@ -225,6 +290,12 @@ function $HttpProvider() { var defaultCache = $cacheFactory('$http'); + /** + * Make sure that default param serializer is exposed as a function + */ + defaults.paramSerializer = isString(defaults.paramSerializer) ? + $injector.get(defaults.paramSerializer) : defaults.paramSerializer; + /** * 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 @@ -636,6 +707,9 @@ function $HttpProvider() { * response body, headers and status and returns its transformed (typically deserialized) version. * See {@link ng.$http#overriding-the-default-transformations-per-request * Overriding the Default Transformations} + * - **paramSerializer** - {string|function(Object):string} - A function used to prepare string representation + * of request parameters (specified as an object). + * Is specified as string, it is interpreted as function registered in with the {$injector}. * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the * GET request, otherwise if a cache instance built with * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for @@ -764,11 +838,14 @@ function $HttpProvider() { var config = extend({ method: 'get', transformRequest: defaults.transformRequest, - transformResponse: defaults.transformResponse + transformResponse: defaults.transformResponse, + paramSerializer: defaults.paramSerializer }, requestConfig); config.headers = mergeHeaders(requestConfig); config.method = uppercase(config.method); + config.paramSerializer = isString(config.paramSerializer) ? + $injector.get(config.paramSerializer) : config.paramSerializer; var serverRequest = function(config) { var headers = config.headers; @@ -1032,7 +1109,7 @@ function $HttpProvider() { cache, cachedResp, reqHeaders = config.headers, - url = buildUrl(config.url, config.params); + url = buildUrl(config.url, config.paramSerializer(config.params)); $http.pendingRequests.push(config); promise.then(removePendingReq, removePendingReq); @@ -1139,27 +1216,9 @@ function $HttpProvider() { } - function buildUrl(url, params) { - if (!params) return url; - var parts = []; - forEachSorted(params, function(value, key) { - if (value === null || isUndefined(value)) return; - if (!isArray(value)) value = [value]; - - forEach(value, function(v) { - if (isObject(v)) { - if (isDate(v)) { - v = v.toISOString(); - } else { - v = toJson(v); - } - } - parts.push(encodeUriQuery(key) + '=' + - encodeUriQuery(v)); - }); - }); - if (parts.length > 0) { - url += ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); + function buildUrl(url, serializedParams) { + if (serializedParams.length > 0) { + url += ((url.indexOf('?') == -1) ? '?' : '&') + serializedParams; } return url; } diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js index a858c83c64bc..a73f2ae75e5f 100644 --- a/test/ng/httpSpec.js +++ b/test/ng/httpSpec.js @@ -3,6 +3,9 @@ describe('$http', function() { var callback, mockedCookies; + var customParamSerializer = function(params) { + return Object.keys(params).join('_'); + }; beforeEach(function() { callback = jasmine.createSpy('done'); @@ -14,6 +17,9 @@ describe('$http', function() { }); }); + beforeEach(module({ + customParamSerializer: customParamSerializer + })); beforeEach(module(function($exceptionHandlerProvider) { $exceptionHandlerProvider.mode('log'); })); @@ -354,6 +360,20 @@ describe('$http', function() { $httpBackend.expect('GET', '/url?date=2014-07-15T17:30:00.000Z').respond(''); $http({url: '/url', params: {date:new Date('2014-07-15T17:30:00.000Z')}, method: 'GET'}); }); + + + describe('custom params serialization', function() { + + it('should allow specifying custom paramSerializer as function', function() { + $httpBackend.expect('GET', '/url?foo_bar').respond(''); + $http({url: '/url', params: {foo: 'fooVal', bar: 'barVal'}, paramSerializer: customParamSerializer}); + }); + + it('should allow specifying custom paramSerializer as function from DI', function() { + $httpBackend.expect('GET', '/url?foo_bar').respond(''); + $http({url: '/url', params: {foo: 'fooVal', bar: 'barVal'}, paramSerializer: 'customParamSerializer'}); + }); + }); }); @@ -1788,11 +1808,16 @@ describe('$http', function() { $httpBackend.flush(); }); - it('should have separate opbjects for defaults PUT and POST', function() { + it('should have separate objects for defaults PUT and POST', function() { expect($http.defaults.headers.post).not.toBe($http.defaults.headers.put); expect($http.defaults.headers.post).not.toBe($http.defaults.headers.patch); expect($http.defaults.headers.put).not.toBe($http.defaults.headers.patch); }); + + it('should expose default param serializer at runtime', function() { + var paramSerializer = $http.defaults.paramSerializer; + expect(paramSerializer({foo: 'foo', bar: ['bar', 'baz']})).toEqual('bar=bar&bar=baz&foo=foo'); + }); }); }); @@ -1929,3 +1954,49 @@ describe('$http with $applyAsync', function() { expect(log).toEqual(['response 1', 'response 2', 'response 3']); }); }); + +describe('$http param serializers', function() { + + var defSer, jqrSer; + beforeEach(inject(function($httpParamSerializer, $httpParamSerializerJQLike) { + defSer = $httpParamSerializer; + jqrSer = $httpParamSerializerJQLike; + })); + + describe('common functionality', function() { + + it('should return empty string for null or undefined params', function() { + expect(defSer(undefined)).toEqual(''); + expect(jqrSer(undefined)).toEqual(''); + expect(defSer(null)).toEqual(''); + expect(jqrSer(null)).toEqual(''); + }); + + it('should serialize objects', function() { + expect(defSer({foo: 'foov', bar: 'barv'})).toEqual('bar=barv&foo=foov'); + expect(jqrSer({foo: 'foov', bar: 'barv'})).toEqual('bar=barv&foo=foov'); + }); + + }); + + describe('default array serialization', function() { + + it('should serialize arrays by repeating param name', function() { + expect(defSer({a: 'b', foo: ['bar', 'baz']})).toEqual('a=b&foo=bar&foo=baz'); + }); + }); + + describe('jquery array and objects serialization', function() { + + it('should serialize arrays by repeating param name with [] suffix', function() { + expect(jqrSer({a: 'b', foo: ['bar', 'baz']})).toEqual('a=b&foo%5B%5D=bar&foo%5B%5D=baz'); + expect(decodeURIComponent(jqrSer({a: 'b', foo: ['bar', 'baz']}))).toEqual('a=b&foo[]=bar&foo[]=baz'); + }); + + it('should serialize objects by repeating param name with [kay] suffix', function() { + expect(jqrSer({a: 'b', foo: {'bar': 'barv', 'baz': 'bazv'}})).toEqual('a=b&foo%5Bbar%5D=barv&foo%5Bbaz%5D=bazv'); + //a=b&foo[bar]=barv&foo[baz]=bazv + }); + }); + +});