Exception Handling and Shielding

O tratamento de erros é um dos aspetos cruciais das aplicações e também, tradicionalmente, um dos mais difíceis de implementar corretamente. Pela sua complexidade, pela multiplicidade de cenários que devem ser considerados e porque há um equilíbrio difícil entre os seguintes vectores:

  • O detalhe e a pertinência das mensagens de erro apresentadas ao utilizador. Devem ajudá-lo a ultrapassar o problema de uma forma clara e sucinta.
  • O detalhe e a pertinência da informação recolhida (para além da apresentada na aplicação) para assistir na resolução de problemas na aplicação (suporte e manutenção).
  • A atenção e esforço necessários por parte do developer (espécie de mamífero pouco disponivel para trabalho pormenorizado como é tipicamente este).

Ainda assim, este é um dos aspetos transversais das aplicações que pode e deve ser endereçado na plataforma, tentando reduzir ao mínimo o trabalho necessário pelo developer, maximizando a informação recolhida sobre o erro e maximizando a qualidade da informação apresentada ao utilizador.

A Plataforma Elevation segue as seguintes orientações principais neste tema:

  • Em circunstância alguma a aplicação pode simplesmente estourar com um erro. Deve tratá-los todos de alguma forma. Mesmo os erros mais inesperados como, por exemplo, a falta de uma dll, deve apresentar uma janela de erro do sistema (ver "yellow screen of hell").
  • Os erros esperados devem ser apresentados ao utilizador dentro da aplicação. Da forma mais adequada a cada erro particular e com mensagens claras e simples que ajudem o utilizador a ultrapassar o problema.
  • Os erros inesperados devem ser apresentados ao utilizador de uma forma especial, indicando-lhe que aconteceu uma situação inesperada que, à partida, ele não poderá resolver.
  • Em circustância alguma os erros apresentados ao utilizador devem apresentar informação sensível ou informação sobre a forma como a aplicação está implementada.

Este último ponto diz respeito ao conceito de Exception Shielding a que a plataforma deve obedecer. A recolha de informação para a resolução de problemas (suporte) não justifica que o utilizador seja confrontado com dados técnicos que não o ajudam. São coisas distintas. Essa informação técnica é recolhida pela aplicação no back-end (tracing, etc.)

Erros Esperados versus Erros Inesperados

Um erro esperado (ExpectedException) corresponde a uma condição que faz parte do funcionamento normal da aplicação e por isso foi antecipada pelo programador. Eis alguns exemplos: - O cliente X não existe. - A condição de pagamento da fatura não está preenchida. - O ficheiro selecionado pelo utilizador não pode ser carregado.

Já um erro inesperado (Exception) será uma condição que não cumpra os requisitos anteriores. Pode ter sido antecipada pelo utilizador mas não é esperado que aconteça na operação normal da aplicação. Exemplos: - O argumento X do método é nulo. - Ocorreu um erro ao ler o ficheiro. - O servidor da base de dados não está disponível.

Qualquer erro esperado deve ser lançado no código através de uma exceção que derive do tipo Primavera.Core.Exceptions.ExpectedException (exemplo: Primavera.Core.Domain.Exceptions.DomainException).

Desse modo será possível distinguir os erros esperados dos inesperados no tratamento de erros:

try
{
    // do something
}
catch (ExpectedException ex)
{
    // show to the user
}
catch (Exception ex)
{
    // show a default dialog for unexpected errors
}

Tratamento dos Erros

Na perspetiva do utilizador (aquilo que ele verá) a Plataforma Elevation tratará os erros da seguinte forma.

Sempre que ocorrer um erro inesperado será apresentado um diálogo semelhante ao seguinte:

Oops!

Este diálogo genérico ocupará toda a página da aplicação quando o erro for, por assim dizer, irrecuperável (não permitir à partida continuar a operação que o causou), ou será apresentado num popup no caso contrário (por exemplo se ocorreu na gravação de um registo).

Todos os erros esperados serão apresentados no contexto da aplicação através do Message Service que apresenta um toast ou um popup consoante o tipo de erro.

Message Service

Tracing

Em ambos os casos é recolhida informação sobre os erros através do Tracing. Os erros inesperados são registados como Error ou Fatal, enquanto os esperados são registados apenas no nível Verbose.

Implementação

Este comportamento é assegurado pelos seguintes componentes da Plataforma.

Exception Handling

Application_Error e GlobalApplicationEventsHandler

No Global.asax do host da aplicação é configurado o evento Application_Error da seguinte forma:

protected void Application_Error(object sender, EventArgs e)
{
    // Notify the global application events handler

    GlobalApplicationEventsHandler.OnError(this);
}

Este evento ocorre sempre que acontecer uma exceção não tratada na aplicação servidor. O GlobalApplicationEventsHandler trata então destes casos assegurando a apresentação do diálogo genérico de erro.

CustomError e Error.Html

Ao nível do ASP.NET os erros estão configurados pelo elemento de configuração customError no web.config:

<customErrors mode="On" defaultRedirect="Error.html" />

A página Error.html (no Host da aplicação) é uma página HTML estática que apresenta o diálogo genérico de erro.

A página Error.html só será utilizada quando ocorrer um erro na aplicação até ao ponto em que os application services não estão carregados. Isso pode acontecer, por exemplo, se o deployment da aplicação estiver errado. Depois de carregados os application services, o tratamento de erros será assegurado pelo serviço ICustomErrorService.

ICustomErrorService

Este serviço é responsável por apresentar o diálogo genérico de erro configurado de acordo com o erro inesperado em causa. Para isso utilizará o controller DefaultErrorController para apresentar a view.

Neste momento são tratados todos os códigos de erro HTTP, sendo que os seguintes têm tratamento particular (apresentando ao utilizador uma mensagem diferente para cada um):

  • 401 - Unauthorized
  • 403 - Forbidden
  • 404 - NotFound
  • 500 - InternalServerError

DefaultErrorController e Error.cshtml

Este controller é responsável por apresentar a view Error.cshtml (do módulo Shell). Essa view é o diálogo genérico de erro.

UnhandledExceptionFilterAttribute

Qualquer exceção ocorrida na camada de serviços (Services Controllers) é tratada por um filtro HTTP associado às ações Web API, sejam as da Web API pública ou as da Web API interna (a usada pelas aplicações).

Esse filtro é definido pela classe Primavera.Core.Services.UnhandledExceptionFilterAttribute e é registado dinamicamente na classe Primavera.Core.Services.HttpServiceConfiguration.

Esta classe é responsável por traduzir exceções não tratadas em mensagens de resposta HTTP segundo as seguintes regras:

  • BaseEntityNotFoundException - traduzida no status code 404 NotFound.
  • EntityNotFoundException - traduzida no status code 404 NotFound.
  • DeletedEntityException - traduzida no status code 405 MethodNotAllowed.
  • DomainValidationException - traduzida no status code 400 BadRequest.
  • ForbiddenException - traduzida no status code 403 Forbidden.
  • ConflictException - traduzida no status code 409 Conflict.
  • ExpectedException - traduzida no status code 400 BadRequest.
  • Qualquer outra - traduzida no status code 500 InternalServerError.

Note-se, neste ponto, que o filtro trata também erros esperados e torna-os em respostas HTTP que o cliente sabe tratar devidamente. Este aspeto é muito importante para o funcionamento da aplicação - o cliente (JS) que processa as respostas e este componente devem estar devidamente alinhados para assegurar o tratamento correto de cada condição de erro.

No caso das exceções ocorridas na Web API pública, o filtro trata também de fazer exception shielding, ou seja, de não apresentar na resposta dados detalhados das exceções. Nesse cenário apenas será incluido na resposta a mensagem de erro e, dependendo do erro, um código de erro (numa propriedade com o nome %ERRORCODE%).

Esse comportamento depende do setting ApplicationContext.Settings.HideExceptionsInPublicWebApi que pode ser modificado na secção de configuração Primavera.Core.Web.ApplicationWebConfigurationSection. Esse setting tem o valor true por defeito.

Outro aspeto muito importante desta implementação é que, uma vez que o filtro trata os erros esperados de uma forma standardizada, não é necessário que o código dos service controllers faça qualquer tratamento especial das exceções (para além daquele que fizer parte da sua própria lógica de negócio).

Note-se que este filtro se aplica apenas a ações Web API e não tem efeito no MVC. Neste último caso o tratamento de erros é assegurado pelo evento Application_Error como qualquer outro erro HTTP.

exceptionHandler.service.js e app.js

No cliente são estes dois componentes que fazem o tratamento dos códigos de erro recebidos do servidor.

  • exceptionHandler.service.js: trata os erros recebidos das ações Web API. Todos os erros com o código 500 são tratados automaticamente de forma genérica, não sendo nunca necessário no código que invoca um serviço fazer qualquer tipo controlo.
  • app.js: trata os outros erros vindos do Application_Error como um HTML com a página de erro.

BusinessExceptionsProcessor

Este componente é responsável pelo processamento de exceções na camada de domínio de acordo com as seguintes regras:

  • ExpectedException - retorna a mesma exceção.
  • Qualquer outra - é encapsulada numa DomainUnexpectedException com uma mensagem de erro genérica.

DataExceptionsProcessor

Este componente é responsável pelo processamento de exceções na camada de dados de acordo com as seguintes regras:

  • ExpectedException - retorna a mesma exceção.
  • SqlException - o código de erro SQL é traduzido numa mensagem preparada para o utilizador e a exceção é encapsulada numa DataException.
  • Qualquer outra - é encapsulada numa DataUnexpectedException com uma mensagem de erro genérica.

Regras de Programação

Os programadores das aplicações devem assegurar as seguintes regras no código custom da aplicação. Essas regras dependem do layer onde estiver o código custom.

Data Layer

Na camada de dados é crucial assegurar a invocação da classe DataExceptionsProcessor da seguinte forma:

public void MyMethod()
{
    try
    {
        // Do something
    }
    catch (Exception ex)
    {
        throw DataExceptionsProcessor.Process(ex);
    }
}

Domain Layer

Na camada de domínio o comportamento deve ser semelhante:

public void MyMethod()
{
    try
    {
        // Do something
    }
    catch (Exception ex)
    {
        throw BusinessExceptionsProcessor.Process(ex);
    }
}

Services Layer

Finalmente nos services controllers devem verificar-se as seguintes normas:

  • Não deve ser programado nenhum tratamento genérico de exceções. Esse é assegurado pelo UnhandledExceptionFilterAttribute.
  • Os erros devem ser lançados como exceções normais. Não é necessário usar a HttpErrorResponseException nem o método Request.CreateErrorResponse.

Diagnóstico

Os erros não tratados incluem um identificador ("Correlation Id") que permite relacionar a mensagem apresentada na aplicação no diálogo de erro com o respetivo registo de tracing.

Este identificador só é registado no trace pelo tracing listener para o AppInsights (TelemetryTracingListener).

Note-se na seguinte mensagem de erro:

Mensagem de Erro com Correlation Id!

O identificador assinalado a vermelho inclui informação adicional sobre o erro:

  • O primeiro número (500) corresponde ao status code HTTP associado ao erro.
  • O segundo identificador é o CorrelationId.

As exceções são registadas pelo tracing listener tanto nos traces como nas exceptions disponíveis no AppInsights.

A seguinte query permite, por exemplo, consultar as exceções ocorridas na última hora:

exceptions
| where timestamp > ago(1h)
| order by timestamp desc  

Exceções no AppInsights!

A coluna customDimensions inclui as seguintes propriedades adicionais sobre a exceção:

  • O Correlation Id.
  • A Source, que corresponde ao handler da aplicação que provocou a recolha da exceção não tratada.

Propriedades da Exceção no AppInsights!

NOTA IMPORTANTE Como já foi referido as exceções são recolhidas na tabela exceptions e na tabela traces do AppInsights. No entanto, as exceções registadas dependem do nível de tracing configurado, ou seja, se o nível configurado for Error, então as exceções recolhidas no tracing em níveis inferiores (Verbose por exemplo) não serão registadas de todo. No caso das exceções não tratadas recolhidas pelos default handlers, elas são sempre registadas com os níveis de trace Error ou Fatal.