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 custom params serializers
Browse files Browse the repository at this point in the history
Closes #3740
Closes #7429
Closes #9224
  • Loading branch information
pkozlowski-opensource committed Apr 2, 2015
1 parent 10ae33b commit 292c757
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 26 deletions.
4 changes: 4 additions & 0 deletions src/AngularPublic.js
Expand Up @@ -67,6 +67,8 @@
$IntervalProvider,
$$HashMapProvider,
$HttpProvider,
$HttpParamSerializerProvider,
$HttpParamSerializerJQLikeProvider,
$HttpBackendProvider,
$LocationProvider,
$LogProvider,
Expand Down Expand Up @@ -224,6 +226,8 @@ function publishExternalAPI(angular) {
$interpolate: $InterpolateProvider,
$interval: $IntervalProvider,
$http: $HttpProvider,
$httpParamSerializer: $HttpParamSerializerProvider,
$httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider,
$httpBackend: $HttpBackendProvider,
$location: $LocationProvider,
$log: $LogProvider,
Expand Down
109 changes: 84 additions & 25 deletions src/ng/http.js
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -174,7 +237,9 @@ function $HttpProvider() {
},

xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN'
xsrfHeaderName: 'X-XSRF-TOKEN',

paramSerializer: '$httpParamSerializer'
};

var useApplyAsync = false;
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
73 changes: 72 additions & 1 deletion test/ng/httpSpec.js
Expand Up @@ -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');
Expand All @@ -14,6 +17,9 @@ describe('$http', function() {
});
});

beforeEach(module({
customParamSerializer: customParamSerializer
}));
beforeEach(module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
}));
Expand Down Expand Up @@ -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'});
});
});
});


Expand Down Expand Up @@ -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');
});
});
});

Expand Down Expand Up @@ -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 292c757

Please sign in to comment.