Bitcoin Accepted
Alberto Pereira
Web & Mobile Developer
Coimbra, Portugal

Angularjs e os posts em diferentes domínios

Num projecto em Angularjs deparei-me com a necessidade seguinte: fazer um pedido, através de um formulário, com o método post, a um api alocado num outro servidor. O problema colocou-se quando comecei a receber respostas de acesso proibido (403) e com um erro de access-control-allow-origin, mais em particular a resposta do servidor não trazia essa directiva no header. Ao confirmar melhor o pedido e a resposta percebeu-se rapidamente que os dados do formulário no pedido estavam a ser enviados em formato json (mimetype application/json) e o api no servidor não suportava este formato mas sim multipart/form-data ou application/x-www-form-urlencoded (os pedidos habituais quando são feitos pelos browsers).

Algum estudo do problema e a conclusão é: o Angularjs não está preparado para alterar a codificação de application/json para qualquer outra. Espectacular...

Duas soluções se apresentam então:
1 - Usar a função jQuery.param(), que cria uma representação serializada de um objecto.
Assim, no meu caso, o serviço correspondente ao pedido é:

.factory('ServiceRequest', ['$http',
  function($http){
      var ServiceRequest = {
        post: function(prms) {
          var promise = $http({
                url: '[URL]', 
                method: "POST",
                data: prms,
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded"
                }
             }).then(function (response) {
                return response.data;
          });
          return promise;
        }
      };
    return ServiceRequest;
}])

De notar que a diferença relativamente a outros casos é explicitamente colocar o mimetype no envio do header - application/x-www-form-urlencoded.
De notar também que recebe os parâmetros a ser enviados, os dados do formulário. Estes já chegam a este serviço serializados, processo esse que prefiro fazer no controlador respectivo:

.controller('nomeCtrl', ['$scope', 'ServiceRequest', function($scope, $serviceRequest) {
    $serviceRequest.post($.param(prms)).then(function(d) {
        // tratar os dados devolvidos (d)
        });
}]);

A variável prms correspondente a um objecto com os dados do formulário { chave1: valor, chave2: valor2, ...} é assim passada pela função $.param() do jQuery, para a normalizar para o envio com o mimetype application/x-www-form-urlencoded.

Esta solução é a mais rápida mas levanta dois problemas: 1 - é necessário incluir mais uma dependência (o jQuery) propositadamente por causa disto. O que é estúpido e não devia ser necessário; e 2 - No caso de usarmos o serviço Resource do Angularjs em detrimento do http - que é, basicamente, uma abstracção do serviço http que permite executar as funções típicas do http directamente num determinado objecto - a função param, se encontrar algum método no objecto (o que, em Angularjs é habitual), e sem pensar duas vezes, executa-a e usa o resultado como mais um elemento da serialização. Um caos portanto.

Não é a melhor solução fazer o override de funções ou comportamentos do core em toda a aplicação.

2 - Criar uma função equivalente à função param na aplicação.

A solução apresentada por estes tipos: http://victorblog.com/2012/12/20/make-angularjs-http-service-behave-like-jquery-ajax/ parece-me a mais adequada, com algumas alterações, na minha opinião. Qualquer solução apresentada será sempre para este caso em particular. Não deverá substituir a estrutura da framework nem o modo como foi pensada. Assim, prefiro sempre fazer as alterações caso a caso:

2.1 - Não acho que se deva substituir o comportamento esperado de um serviço existente. Enquanto no exemplo do link acima a proposta é alterar as variáveis default do serviço http usando a seguinte linha de código na configuração da aplicação 

$httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';

eu prefiro a solução de, na função do serviço que efectua a comunicação (o POST) colocar explicitamente o tipo de header que pretendemos, ficando o serviço exatamente como apresentei na solução anterior (1).

2.2 - Uma vez mais a solução apresentada pelos tipos propõe sobrepor a função transformRequest do serviço http no sentido de lhe colocar lá a nova função param, criada localmente.

// Override $http service's default transformRequest
$httpProvider.defaults.transformRequest = [function(data) {
    return angular.isObject(data) && String(data) !== '[object File]' ? param(data) : data;
}];

Não acho que seja a melhor solução. É muito mais "limpo" criar um novo serviço com a função param e injectá-lo no controlador, mantendo-se, também, o controlador praticamente igual à versão apresentada no ponto 1.
O serviço criado fica então:

.factory('Utils', function () {
    return {
        /**
        * Converts an object to x-www-form-urlencoded serialization.
        * @param {Object} obj
        * @return {String}
        */ 
        param: function (obj) {
            var query = '', name, value, fullSubName, subName, subValue, innerObj, i;
            for(name in obj) {
                value = obj[name];
                if(value instanceof Array) {
                    for(i=0; i<value.length; ++i) {
                        subValue = value[i];
                        fullSubName = name + '[' + i + ']';
                        innerObj = {};
                        innerObj[fullSubName] = subValue;
                        query += param(innerObj) + '&';
                    }
                }
                else if(value instanceof Object) {
                    for(subName in value) {
                        subValue = value[subName];
                        fullSubName = name + '[' + subName + ']';
                        innerObj = {};
                        innerObj[fullSubName] = subValue;
                        query += param(innerObj) + '&';
                    }
                }
                else if(value !== undefined && value !== null)
                query += encodeURIComponent(name) + '=' + encodeURIComponent(value) + '&';
            }
            return query.length ? query.substr(0, query.length - 1) : query;
        }
    }
});

E, no controlador, basta injectar este serviço e substituir a função param:

.controller('nomeCtrl', ['$scope', 'ServiceRequest', 'Utils', function($scope, $serviceRequest, $utils) {
    $serviceRequest.post($utils.param(prms)).then(function(d) {
        // tratar os dados devolvidos (d)
    });
}]);

Parece-me que é a solução que melhor responde aos problemas levantados: substituir a função jQuery.param() tentando ao máximo mimetizar o código como se ela lá estivesse.

Alberto Pereira | 3-12-2014
Category: post - Tag: angularjs
Comentário(s)

-

comments powered by Disqus