Permalink
Browse files

feat($http): support custom params serializers

Closes #3740
Closes #7429
Closes #9224
Closes #11461
  • Loading branch information...
1 parent 731c8b5 commit 6c8464ad14dd308349f632245c1a064c9aae242a @pkozlowski-opensource pkozlowski-opensource committed Mar 30, 2015
Showing with 160 additions and 26 deletions.
  1. +4 −0 src/AngularPublic.js
  2. +84 −25 src/ng/http.js
  3. +72 −1 test/ng/httpSpec.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,
View
@@ -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,string>):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
@@ -226,6 +291,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
* server request.
@@ -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,string>):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;
}
View
@@ -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
+ });
+ });
+
+});

0 comments on commit 6c8464a

Please sign in to comment.