3

In my Angular app, I have a service that utilizes $http to retrieve data from a server. The server endpoint uses HMAC authentication and expects the query string parameters to be in a specific order on the URL.

Angular sorts the $http parameters when it builds the URL, so it doesn't seem possible to specify a custom parameter order.

Here's an example:

this.apiCall = function() {
    return $http({
        method: 'GET',
        url: 'http://example.com/url/v1/endpoint',
        params: {
            'c': 'cdata',
            'a': 'adata',
            'b': 'bdata'
        }
    });
};

Angular will build the URL as http://example.com/url/v1/endpoint?a=adata&b=bdata&c=cdata, but I need to preserve the order of the params as specified, http://example.com/url/v1/endpoint?c=cdata&a=adata&b=bdata.

I realize I could just tack on the parameters to the URL string manually, but that is not very friendly and it doesn't allow for easy management in $http interceptors.

Angular probably sorts the parameters to keep a uniform behavior across browser implementations, as object ordering is not specified in ECMAScript.

Regardless, does anyone know how to work around the default Angular behavior of sorting the params in order to build a URL that preserves the parameters as specified?

Stephen Watkins
  • 25,047
  • 15
  • 66
  • 100
  • Seems to me, you've answered your question already. Looking at the linked source code, it's apparent, that the only way to enforce parameter order is to construct the query string yourself and supply it as part of the URL. You could create a decorator for `$http` transforming configuration on encountering some special `params` syntax. – hon2a Nov 25 '14 at 15:55
  • @hon2a Yep, I ended up creating a decorator to custom build the URL. – Stephen Watkins Nov 25 '14 at 18:15

2 Answers2

3

I improved upon your solution to create something more stream-lined and guaranteed to work:

$httpProvider.interceptors.push(function() {
    return {
        request: function (config) {
            if (!config.paramOrder) {
                return config;
            }

            // avoid leaking config modifications
            config = angular.copy(config, {});

            var orderedParams = [];
            config.paramOrder.forEach(function (key) {
                if (config.params.hasOwnProperty(key)) {
                    orderedParams.push(encodeURIComponent(key) + '=' + encodeURIComponent(config.params[key]));
                    // leave only the unordered params in the `params` config
                    delete config.params[key];
                }
            });

            config.url += (config.url.indexOf('?') === -1) ? '?' : '&';
            config.url += orderedParams.join('&');

            return config;
        },
    };
});

Invoke with the following:

$http({
    method: 'GET',
    url: 'http://example.com/url/v1/endpoint',
    params: {
        a: 'aValue',
        b: 'bValue',
        c: 'cValue'
    },
    paramOrder: ['c', 'a']
});

to get a query string starting with key c followed by a. The params not mentioned in paramOrder will be appended after the ordered params (in alphabetical order).

hon2a
  • 7,006
  • 5
  • 41
  • 55
1

I ended up creating a rudimentary interceptor to keep the "as-specified" params order. This interceptor runs if the keepParamsOrder configuration variable is set on the $http call.

In your module config:

$httpProvider.interceptors.push(function() {
    return {
        'request': function(config) {
            if (!config.keepParamsOrder || !config.params) {
                return config;
            }

            var queryStrings = [];
            for (var key in config.params) {
                if (config.params.hasOwnProperty(key)) {
                    queryStrings.push(key + '=' + config.params[key]);
                }
            }

            // Reset the params to be empty
            config.params = {};

            config.url += (config.url.indexOf('?') === -1) ? '?' : '&';
            config.url += queryStrings.join('&');

            return config;
        },
    };
});

Tell it to run in the service call config:

this.apiCall = function() {
    return $http({
        method: 'GET',
        url: 'http://example.com/url/v1/endpoint',
        params: {
            'c': 'cdata',
            'a': 'adata',
            'b': 'bdata'
        },
        keepParamsOrder: true
    });
};
Stephen Watkins
  • 25,047
  • 15
  • 66
  • 100
  • Your solution is somehow lacking, as you're still passing the `params` in an object, which is an [unordered collection](http://stackoverflow.com/questions/5525795/does-javascript-guarantee-object-property-order). Instead of adding `keepParamsOrder`, you could either recognize a special `params` syntax (involving an `Array`) or add `paramOrder` config containing names of the `params` you want ordered. – hon2a Nov 25 '14 at 19:17