Partilhar via


Tratamento global de erros no ASP.NET Web API 2

por David Matson, Rick Anderson

Este tópico fornece uma visão geral do tratamento de erros global no ASP.NET Web API 2 para ASP.NET 4.x. Hoje não há uma maneira fácil na API Web de registrar ou lidar com erros globalmente. Algumas exceções sem tratamento podem ser processadas por meio de filtros de exceção, mas há vários casos que os filtros de exceção não podem manipular. Por exemplo:

  1. Exceções geradas por construtores de controlador.
  2. Exceções geradas por manipuladores de mensagens.
  3. Exceções geradas durante o roteamento.
  4. Exceções geradas durante a serialização de conteúdo da resposta.

Queremos fornecer uma maneira simples e consistente de registrar e manipular (sempre que possível) essas exceções.

Há dois casos principais para lidar com exceções, o caso em que podemos enviar uma resposta de erro e o caso em que tudo o que podemos fazer é registrar a exceção. Um exemplo para o último caso é quando uma exceção é lançada no meio do conteúdo de resposta de streaming; nesse caso, é tarde demais para enviar uma nova mensagem de resposta, pois o código status, cabeçalhos e conteúdo parcial já passaram pelo fio, portanto, simplesmente anulamos a conexão. Embora a exceção não possa ser tratada para produzir uma nova mensagem de resposta, ainda há suporte para registrar a exceção em log. Nos casos em que podemos detectar um erro, podemos retornar uma resposta de erro apropriada, conforme mostrado no seguinte:

public IHttpActionResult GetProduct(int id)
{
    var product = products.FirstOrDefault((p) => p.Id == id);
    if (product == null)
    {
        return NotFound();
    }
    return Ok(product);
}

Opções existentes

Além dos filtros de exceção, os manipuladores de mensagens podem ser usados hoje para observar todas as respostas de 500 níveis, mas agir nessas respostas é difícil, pois eles não têm contexto sobre o erro original. Os manipuladores de mensagens também têm algumas das mesmas limitações que os filtros de exceção em relação aos casos que podem lidar. Embora a API Web tenha infraestrutura de rastreamento que captura condições de erro, a infraestrutura de rastreamento é para fins diagnóstico e não é projetada ou adequada para execução em ambientes de produção. O tratamento e o registro em log de exceções globais devem ser serviços que podem ser executados durante a produção e conectados a soluções de monitoramento existentes (por exemplo, ELMAH).

Visão geral da solução

Fornecemos dois novos serviços substituíveis pelo usuário, IExceptionLogger e IExceptionHandler, para registrar e lidar com exceções sem tratamento. Os serviços são muito semelhantes, com duas diferenças main:

  1. Damos suporte ao registro de vários agentes de exceção, mas apenas a um único manipulador de exceção.
  2. Os agentes de exceção sempre são chamados, mesmo que estejamos prestes a anular a conexão. Manipuladores de exceção só são chamados quando ainda podemos escolher qual mensagem de resposta enviar.

Ambos os serviços fornecem acesso a um contexto de exceção que contém informações relevantes do ponto em que a exceção foi detectada, especialmente o HttpRequestMessage, o HttpRequestContext, a exceção gerada e a fonte de exceção (detalhes abaixo).

Princípios de design

  1. Nenhuma alteração interruptiva Como essa funcionalidade está sendo adicionada em uma versão secundária, uma restrição importante que afeta a solução é que não há alterações interruptivas, seja para digitar contratos ou comportamento. Essa restrição descartou alguma limpeza que gostaríamos de ter feito em termos de blocos catch existentes transformando exceções em 500 respostas. Essa limpeza adicional é algo que podemos considerar para uma versão principal subsequente.
  2. Mantendo a consistência com constructos de API Web O pipeline de filtro da API Web é uma ótima maneira de lidar com preocupações transversais com a flexibilidade de aplicar a lógica em um escopo global, específico do controlador ou específico da ação. Os filtros, incluindo filtros de exceção, sempre têm contextos de ação e controlador, mesmo quando registrados no escopo global. Esse contrato faz sentido para filtros, mas significa que os filtros de exceção, mesmo os de escopo global, não são adequados para alguns casos de tratamento de exceção, como exceções de manipuladores de mensagens, em que não existe nenhuma ação ou contexto do controlador. Se quisermos usar o escopo flexível proporcionado por filtros para tratamento de exceções, ainda precisaremos de filtros de exceção. Mas, se precisarmos lidar com a exceção fora de um contexto de controlador, também precisamos de um constructo separado para tratamento de erros global completo (algo sem as restrições de contexto de ação e contexto de ação do controlador).

Quando usar

  • Os agentes de exceção são a solução para ver todas as exceções sem tratamento capturadas pela API Web.
  • Manipuladores de exceção são a solução para personalizar todas as respostas possíveis para exceções sem tratamento capturadas pela API Web.
  • Os filtros de exceção são a solução mais fácil para processar as exceções sem tratamento de subconjunto relacionadas a uma ação ou controlador específico.

Detalhes do serviço

O agente de exceção e as interfaces de serviço de manipulador são métodos assíncronos simples que tomam os respectivos contextos:

public interface IExceptionLogger
{
   Task LogAsync(ExceptionLoggerContext context, 
                 CancellationToken cancellationToken);
}

public interface IExceptionHandler
{
   Task HandleAsync(ExceptionHandlerContext context, 
                    CancellationToken cancellationToken);
}

Também fornecemos classes base para ambas as interfaces. Substituir os métodos principais (sincronização ou assíncrono) é tudo o que é necessário para registrar ou manipular nos horários recomendados. Para registro em log, a ExceptionLogger classe base garantirá que o método de registro em log principal seja chamado apenas uma vez para cada exceção (mesmo que posteriormente se propague ainda mais na pilha de chamadas e seja capturado novamente). A ExceptionHandler classe base chamará o método de tratamento principal apenas para exceções na parte superior da pilha de chamadas, ignorando blocos de captura aninhados herdados. (Versões simplificadas dessas classes base estão no apêndice abaixo.) E IExceptionLoggerIExceptionHandler recebem informações sobre a exceção por meio de um ExceptionContext.

public class ExceptionContext
{
   public Exception Exception { get; set; }

   public HttpRequestMessage Request { get; set; }

   public HttpRequestContext RequestContext { get; set; }

   public HttpControllerContext ControllerContext { get; set; }

   public HttpActionContext ActionContext { get; set; }

   public HttpResponseMessage Response { get; set; }

   public string CatchBlock { get; set; }

   public bool IsTopLevelCatchBlock { get; set; }
}

Quando a estrutura chama um agente de exceção ou um manipulador de exceção, ela sempre fornecerá um Exception e um Request. Com exceção do teste de unidade, ele também sempre fornecerá um RequestContext. Ele raramente fornecerá um ControllerContext e ActionContext (somente ao chamar do bloco catch para filtros de exceção). Ele raramente fornecerá um Response(somente em determinados casos do IIS quando estiver no meio da tentativa de gravar a resposta). Observe que, como algumas dessas propriedades podem sernull, cabe ao consumidor marcar antes null de acessar membros da classe de exceção.CatchBlock é uma cadeia de caracteres que indica qual bloco catch viu a exceção. As cadeias de caracteres do bloco catch são as seguintes:

  • HttpServer (método SendAsync)

  • HttpControllerDispatcher (método SendAsync)

  • HttpBatchHandler (método SendAsync)

  • IExceptionFilter (processamento da ApiController do pipeline de filtro de exceção em ExecuteAsync)

  • Host OWIN:

    • HttpMessageHandlerAdapter.BufferResponseContentAsync (para saída de buffer)
    • HttpMessageHandlerAdapter.CopyResponseContentAsync (para saída de streaming)
  • Host da Web:

    • HttpControllerHandler.WriteBufferedResponseContentAsync (para saída de buffer)
    • HttpControllerHandler.WriteStreamedResponseContentAsync (para saída de streaming)
    • HttpControllerHandler.WriteErrorResponseContentAsync (para falhas na recuperação de erro no modo de saída em buffer)

A lista de cadeias de caracteres de bloco catch também está disponível por meio de propriedades estáticas somente leitura. (A cadeia de caracteres de bloco de captura principal estática ExceptionCatchBlocks; o restante aparece em uma classe estática para o OWIN e o host da Web).IsTopLevelCatchBlock é útil para seguir o padrão recomendado de tratamento de exceções somente na parte superior da pilha de chamadas. Em vez de transformar exceções em 500 respostas em qualquer lugar em que ocorra um bloco de captura aninhado, um manipulador de exceção pode permitir que exceções se propaguem até que elas estejam prestes a ser vistas pelo host.

Além do ExceptionContext, um agente obtém mais uma informação por meio do completo ExceptionLoggerContext:

public class ExceptionLoggerContext
{
   public ExceptionContext ExceptionContext { get; set; }
   public bool CanBeHandled { get; set; }
}

A segunda propriedade, CanBeHandled, permite que um agente identifique uma exceção que não pode ser tratada. Quando a conexão estiver prestes a ser anulada e nenhuma nova mensagem de resposta puder ser enviada, os agentes serão chamados, mas o manipulador não será chamado e os agentes poderão identificar esse cenário dessa propriedade.

No adicional ao ExceptionContext, um manipulador obtém mais uma propriedade que pode definir na íntegra ExceptionHandlerContext para lidar com a exceção:

public class ExceptionHandlerContext
{
   public ExceptionContext ExceptionContext { get; set; }
   public IHttpActionResult Result { get; set; }
}

Um manipulador de exceção indica que ele lidou com uma exceção definindo a Result propriedade como um resultado de ação (por exemplo, um ExceptionResult, InternalServerErrorResult, StatusCodeResult ou um resultado personalizado). Se a Result propriedade for nula, a exceção será sem tratamento e a exceção original será lançada novamente.

Para exceções na parte superior da pilha de chamadas, demos uma etapa extra para garantir que a resposta seja apropriada para os chamadores de API. Se a exceção se propagar para o host, o chamador verá a tela amarela de morte ou alguma outra resposta fornecida pelo host que normalmente é HTML e geralmente não é uma resposta de erro de API apropriada. Nesses casos, o Resultado inicia não nulo e somente se um manipulador de exceção personalizado o definir explicitamente como null (sem tratamento) a exceção será propagada para o host. Definir Result como null nesses casos pode ser útil para dois cenários:

  1. API Web hospedada por OWIN com o middleware de tratamento de exceção personalizado registrado antes/fora da API Web.
  2. Depuração local por meio de um navegador, em que a tela amarela da morte é, na verdade, uma resposta útil para uma exceção sem tratamento.

Para agentes de exceção e manipuladores de exceção, não faremos nada para recuperar se o agente ou o próprio manipulador gerar uma exceção. (Além de deixar a exceção se propagar, deixe os comentários na parte inferior desta página se você tiver uma abordagem melhor.) O contrato para agentes e manipuladores de exceção é que eles não devem permitir que exceções se propaguem para seus chamadores; caso contrário, a exceção apenas será propagada, muitas vezes até o host, resultando em um erro HTML (como ASP. Tela amarela do NET) sendo enviado de volta para o cliente (que geralmente não é a opção preferencial para chamadores de API que esperam JSON ou XML).

Exemplos

Agente de Exceção de Rastreamento

O agente de exceção abaixo envia dados de exceção para fontes de rastreamento configuradas (incluindo a janela de saída de depuração no Visual Studio).

class TraceExceptionLogger : ExceptionLogger
{
    public override void LogCore(ExceptionLoggerContext context)
    {
        Trace.TraceError(context.ExceptionContext.Exception.ToString());
    }
}

Manipulador de exceção de mensagem de erro personalizado

O manipulador de exceção abaixo produz uma resposta de erro personalizada aos clientes, incluindo um endereço de email para contatar o suporte.

class OopsExceptionHandler : ExceptionHandler
{
    public override void HandleCore(ExceptionHandlerContext context)
    {
        context.Result = new TextPlainErrorResult
        {
            Request = context.ExceptionContext.Request,
            Content = "Oops! Sorry! Something went wrong." +
                      "Please contact support@contoso.com so we can try to fix it."
        };
    }

    private class TextPlainErrorResult : IHttpActionResult
    {
        public HttpRequestMessage Request { get; set; }

        public string Content { get; set; }

        public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
        {
            HttpResponseMessage response = 
                             new HttpResponseMessage(HttpStatusCode.InternalServerError);
            response.Content = new StringContent(Content);
            response.RequestMessage = Request;
            return Task.FromResult(response);
        }
    }
}

Registrando filtros de exceção

Se você usar o modelo de projeto "aplicativo Web ASP.NET MVC 4" para criar seu projeto, coloque o código de configuração da API Web dentro da WebApiConfig classe na pasta App_Start :

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new ProductStore.NotImplExceptionFilterAttribute());

        // Other configuration code...
    }
}

Apêndice: Detalhes da Classe Base

public class ExceptionLogger : IExceptionLogger
{
    public virtual Task LogAsync(ExceptionLoggerContext context, 
                                 CancellationToken cancellationToken)
    {
        if (!ShouldLog(context))
        {
            return Task.FromResult(0);
        }

        return LogAsyncCore(context, cancellationToken);
    }

    public virtual Task LogAsyncCore(ExceptionLoggerContext context, 
                                     CancellationToken cancellationToken)
    {
        LogCore(context);
        return Task.FromResult(0);
    }

    public virtual void LogCore(ExceptionLoggerContext context)
    {
    }

    public virtual bool ShouldLog(ExceptionLoggerContext context)
    {
        IDictionary exceptionData = context.ExceptionContext.Exception.Data;

        if (!exceptionData.Contains("MS_LoggedBy"))
        {
            exceptionData.Add("MS_LoggedBy", new List<object>());
        }

        ICollection<object> loggedBy = ((ICollection<object>)exceptionData[LoggedByKey]);

        if (!loggedBy.Contains(this))
        {
            loggedBy.Add(this);
            return true;
        }
        else
        {
            return false;
        }
    }
}

public class ExceptionHandler : IExceptionHandler
{
    public virtual Task HandleAsync(ExceptionHandlerContext context, 
                                    CancellationToken cancellationToken)
    {
        if (!ShouldHandle(context))
        {
            return Task.FromResult(0);
        }

        return HandleAsyncCore(context, cancellationToken);
    }

    public virtual Task HandleAsyncCore(ExceptionHandlerContext context, 
                                       CancellationToken cancellationToken)
    {
        HandleCore(context);
        return Task.FromResult(0);
    }

    public virtual void HandleCore(ExceptionHandlerContext context)
    {
    }

    public virtual bool ShouldHandle(ExceptionHandlerContext context)
    {
        return context.ExceptionContext.IsOutermostCatchBlock;
    }
}