Neste artigo eu mostrei como construir serviços WCF para serem consumidos através do jQuery. Nele falamos sobre os cuidados que devemos ter para criar e expor serviços WCF, mas não chegamos a falar sobre alguns detalhes, não menos importante, como é o caso do tratamento de erros, que é algo tão comum em qualquer tipo de aplicação.
Ao executar alguma operação, uma exceção pode ser disparada por algum motivo. Como já falamos, ao utilizar a biblioteca do jQuery para invocar algum serviço, podemos através de um callback, especificarmos um código para ser disparado quando algum problema ocorrer do lado do serviço. Para configurar isso, podemos utilizar o parâmetro error da função $.ajax. O código abaixo ilustra como podemos configurar o parâmetro error, exibindo uma mensagem informando ao usuário que algum problema ocorreu:
function RecuperarUsuario() {
$.ajax(
{
type: “POST”,
url: “http://localhost/Services/ServicoDeUsuarios.svc/RecuperarUsuario”,
contentType: “application/json”,
data: ‘{ “nome”: “”, “email”: “ia@israelaece.com” }’, //Nome vazio causa o erro
processData: false,
success:
function (resultado) {
alert(resultado.RecuperarUsuarioResult.Nome);
alert(resultado.RecuperarUsuarioResult.Email);
},
error:
function (xhr, textStatus, errorThrown) {
alert(‘Algum Problema Ocorreu!’);
}
});
}
Esse tipo de código funciona normalmente, até que você precise capturar a mensagem exata do erro que ocorreu. Quando executamos uma operação que está disparando alguma exceção, o WCF acaba retornando a seguinte mensagem: “Request server encountered an error processing the request. See server logs for more details”. O problema é que o erro não será retornado em um formato que é facilmente entendido/preferido pelo jQuery, que é o JSON.
Ao configurar o contrato para expor via AJAX, podemos definir através do atributo WebInvokeAttribute, que a requisição (RequestFormat) e a resposta (ResponseFormat) sejam formatadas em JSON, mas isso não inclui eventuais exceções que sejam disparadas pelas respectivas operações, mesmo que esse problema esteja definido como um contrato de fault (FaultContractAttribute).
Para tentar resolver isso, podemos recorrer a alguns pontos de estensibilidade do WCF, para conseguirmos interceptar o erro que ocorreu. Ao interceptar, deveremos transformar a mensagem em algum objeto que deverá ser entendido pelo jQuery, entregando para o cliente todas as informações necessárias para que o mesmo consiga tratar o erro ocorrido.
Para descrever o problema, vamos criar uma classe que possui características que detalham o erro ocorrido. Essa classe será chamada de GenericErrorDescription, que possuirá apenas uma única propriedade (para manter a simplicidade), chamada de Message, do tipo string. Como a finalidade será transformar a exceção em formato JSON para facilitar o consumo pelo jQuery, criaremos uma classe chamada de JsonErrorHandler, e nela implementaremos a interface IErrorHandler, que é uma espécie de interceptador (mais detalhes neste artigo). O principal método fornecido por esta interface, é chamado de ProvideFault, que passa como parâmetro a instância da exceção que ocorreu e uma instância da classe Message, que corresponde a mensagem que será devolvida para o cliente.
Na implementação deste método, utilizaremos o método estático CreateMessage da classe Message, que criará a mensagem de retorno, utilizando o serializador JSON para que a mesma será formatada neste padrão, e além disso, esse serializador deverá serializar a instância da classe que representa o erro, que no nosso exemplo é a GenericErrorDescription. Na sequência configuramos a instância da classe HttpResponseMessageProperty, para customizarmos a mensagem, informando que o tipo a ser retornado será application/json, para que o jQuery consiga, facilmente, entender e interpretar esse conteúdo. Note também que mantemos o StatusCode do HTTP como 500, ou seja, um erro interno, e como sua descrição, definimos a mesma mensagem gerada pela exceção disparada pela operação.
A classe JsonErrorHandler ainda implementa a interface IEndpointBehavior, que nos permite acoplar a instância desta classe em um endpoint específico, do lado do serviço (dispatcher), na coleção de tratadores de erros. O código abaixo ilustra a classe JsonErrorHandler já devidamente implementada:
public class JsonErrorHandler : IEndpointBehavior, IErrorHandler
{
public bool HandleError(Exception error)
{
return true;
}
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
GenericErrorDescription desc =
new GenericErrorDescription() { Message = error.Message };
fault = Message.CreateMessage(version, “”, desc,
new DataContractJsonSerializer(typeof(GenericErrorDescription)));
fault.Properties.Add(WebBodyFormatMessageProperty.Name,
new WebBodyFormatMessageProperty(WebContentFormat.Json));
var msgp = new HttpResponseMessageProperty();
msgp.Headers[HttpResponseHeader.ContentType] = “application/json”;
msgp.StatusCode = HttpStatusCode.InternalServerError;
msgp.StatusDescription = desc.Message;
fault.Properties.Add(HttpResponseMessageProperty.Name, msgp);
}
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bp) { }
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime cr) { }
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher ed)
{
ed.ChannelDispatcher.ErrorHandlers.Add(this);
}
public void Validate(ServiceEndpoint endpoint) { }
}
Só que essa classe por si só não funciona. Precisamos efetivamente acoplar ao pipeline do WCF, e para isso, podemos criar um extension element, que nos dará a oportunidade de efetuar essa configuração de modo declarativo, ou seja, através do arquivo Web.config. Basicamente, essa classe será responsável por criar instâncias da classe que criamos acima, ou seja, do tratador de erros:
public class JsonErrorElement : BehaviorExtensionElement
{
public override Type BehaviorType
{
get
{
return typeof(JsonErrorHandler);
}
}
protected override object CreateBehavior()
{
return new JsonErrorHandler();
}
}
Depois das classes devidamente criadas, tudo o que precisamos fazer agora é configurarmos o arquivo Web.config do serviço, acoplando o extension element ao endpoint de acesso ao serviço. A configuração final do Web.config deve ficar da seguinte forma:
<system.serviceModel>
<services>
<service name=”ServicoDeUsuarios” behaviorConfiguration=”config“>
<endpoint
binding=”webHttpBinding”
contract=”IUsuarios”
behaviorConfiguration=”edpConfig” />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name=”config“>
<serviceDebug includeExceptionDetailInFaults=”false” />
</behavior>
</serviceBehaviors>
<endpointBehaviors>
<behavior name=”edpConfig“>
<jsonErrorHandler />
</behavior>
</endpointBehaviors>
</behaviors>
<extensions>
<behaviorExtensions>
<add
name=”jsonErrorHandler”
type=”JsonErrorElement, WCFExtensions, Version=1.0.0.0, …” />
</behaviorExtensions>
</extensions>
</system.serviceModel>
Já do lado do cliente, as mudanças são bastante ligeiras. Tudo o que precisamos fazer é uma pequena mudança no código que é disparado quando o erro ocorre. Uma vez que o interceptador de erros está acoplado do WCF, a mensagem com o erro de retorno será formatada em JSON, e se analisarmos o retorno, veremos o seguinte resultado:
{“Message”: “Informe o nome do usuariou000du000aParameter name: nome”}
Como a mensagem de retorno está em formato JSON, utilizaremos o método parse da classe JSON, exposta pelo JSON2. Esse método é responsável por transformar o conteúdo JSON em um objeto, respeitando as mesmas propriedades serializadas pela classe GenericErrorDescription. Com isso, o nosso código de consumo ao serviço será alterado para:
function RecuperarUsuario() {
$.ajax(
{
type: “POST”,
url: “http://localhost/Services/ServicoDeUsuarios.svc/RecuperarUsuario”,
contentType: “application/json”,
data: ‘{ “nome”: “”, “email”: “ia@israelaece.com” }’, //Nome vazio causa o erro
processData: false,
success:
function (resultado) {
alert(resultado.RecuperarUsuarioResult.Nome);
alert(resultado.RecuperarUsuarioResult.Email);
},
error:
function (xhr, textStatus, errorThrown) {
var erro = JSON.parse(xhr.responseText);
alert(erro.Message);
}
});
}
Se quiser levar mais detalhes do erro, você ainda tem algumas outras alternativas, mas não tão interessantes como essa. Você poderia optar por definir o atributo includeExceptionDetailInFaults do elemento serviceDebug para True, mas isso iria expor mais informações do que realmente deveria, como por exemplo a Stack Trace, algo que não deveria ultrapassar o serviço. Além dela, ainda poderia envolver toda a operação em um bloco try/catch, e caso algum problema ocorra, utilizamos a classe WebOperationContext para customizar o StatusCode e o ContentType através dela, mas isso torna a classe que representa o serviço dependente do protocolo HTTP, o que não é uma boa opção.
Conclusão: Graças a grande flexibilidade fornecida pelo WCF, podemos contornar alguma de suas “limitações” de uma forma bastante elegante, sem precisar misturar em minhas operações, códigos que estão relacionados puramente à infraestrutura.
Falae Israel,
ainda neste assunto, você já viu alguma vez um serviço nesta configuração funcionar localmente (cassini), mas não no servidor (IIS7)?
No meu caso, quando faço o deploy o serviço retorna 200 como StatusCode ao invés de retornar o que eu defini e dessa forma a mensagem de erro não vem. Eu fiz remote debugging (como uma última opção) e o pipeline de execução está correto. As classes de handler custom estão sendo executadas e tudo deveria estar funcionando, mas não está.
Alguma idéia?
Excelente Israel! Me ajudou muito!
No entanto, uma curiosidade que talvez você não saiba. No seu exemplo você tem apenas um Element no seu Behavior que é o jsonErrorHandler. No meu cenário, além deste utilizo também o webHttp e descobri do pior jeito que a ordem em que você adicionar os Elements no behavior faz diferença.
Quando você usa dessa forma não funciona:
<behavior name="Rest">
<JSonErrorHandler/>
<webHttp/>
</behavior>
Mas se inverter… ai vai!
<behavior name="Rest">
<webHttp/>
<JSonErrorHandler/>
</behavior>
Boas tucaz,
Tanto o JsonErrorHandler que criamos quanto o WebHttpBehavior implementam a interface IEndpointBehavior, que por sua vez, permite-nos inserir um tratador de erros customizado (uma classe que implemente a interface IErrorHandler). Internamente, o WCF adiciona (via WebHttpBehavior) uma instância da classe WebErrorHandler, que mostra a página de erro.
Repare que o método HandleError (IErrorHandler) deve retornar um valor booleano. Ao definir como False, ele permitirá que outros error handlers possam ser processados. Lembre-se de que agora temos dois deles adicionados (WebErrorHandler e o JsonErrorHandler). Como eu defini o retorno do método HandleError como True, ele está dizendo para o WCF que o erro já está tratado e que não deve passar a mensagem adiante, para os próximos elementos da coleção de handlers tentar efetuar o tratamento.
Eu suspeito de que se você definir o HandleError como False, provavelmente o WebHttpBehavior também será executado, mesmo estando definido depois do tratador do Json na coleção de behaviors do serviço.
Boas tucaz,
Já tentou apagar todos os handlers adicionados:
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher ed)
{
ed.ChannelDispatcher.ErrorHandlers.Clear();
ed.ChannelDispatcher.ErrorHandlers.Add(this);
}
Só que não adianta fazer isso se ele é adicionado antes do WebHttpBehavior.