Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
por David Matson, Rick Anderson
Este tópico fornece uma visão geral do tratamento global de erros na Web API 2 ASP.NET para ASP.NET 4.x. Hoje em dia, não há uma forma fácil na Web API de registar ou tratar erros globalmente. Algumas exceções não tratadas podem ser processadas através de filtros de exceção, mas há vários casos que os filtros de exceção não conseguem gerir. Por exemplo:
- Exceções lançadas a partir de construtores de classe de controlador.
- Exceções lançadas de manipuladores de mensagens.
- Exceções lançadas durante o roteamento.
- Exceções lançadas durante a serialização do conteúdo de resposta.
Queremos fornecer uma forma simples e consistente de registar e gerir (sempre que possível) estas exceções.
Existem dois casos principais para lidar com exceções: o caso em que conseguimos enviar uma resposta de erro e o caso em que tudo o que podemos fazer é registar a exceção. Um exemplo deste último caso é quando uma exceção é lançada no meio de um conteúdo de resposta em streaming; Nesse caso, já é tarde demais para enviar uma nova mensagem de resposta, pois o código de estado, os cabeçalhos e o conteúdo parcial já passaram pelo fio, por isso simplesmente abortamos a ligação. Embora a exceção não possa ser tratada para produzir uma nova mensagem de resposta, continuamos a suportar o registo da exceção. Nos casos em que conseguimos detetar um erro, podemos devolver uma resposta de erro apropriada, como 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
Para além dos filtros de exceção, hoje em dia os manipuladores de mensagens podem ser usados para observar todas as respostas de nível 500, mas agir sobre essas respostas é difícil, pois lhes falta contexto sobre o erro original. Os manipuladores de mensagens também têm algumas das mesmas limitações dos filtros de exceção relativamente aos casos que podem tratar. Embora a Web API tenha infraestrutura de rastreio que capta condições de erro, a infraestrutura de rastreio destina-se a fins de diagnóstico e não é concebida nem adequada para correr em ambientes de produção. O tratamento global de exceções e o registo devem ser serviços que podem ser executados durante a produção e integrados em soluções de monitorização existentes (por exemplo, ELMAH).
Descrição Geral das Soluções
Disponibilizamos dois novos serviços substituíveis pelo utilizador, IExceptionLogger e IExceptionHandler, para registar e tratar exceções não tratadas. Os serviços são muito semelhantes, com duas diferenças principais:
- Suportamos o registo de múltiplos registos de exceções, mas apenas um único gestor de exceções.
- Os registadores de exceções são sempre chamados, mesmo que estejamos prestes a abortar a ligação. Os gestores de exceções só são chamados quando ainda conseguimos escolher qual a mensagem de resposta a enviar.
Ambos os serviços fornecem acesso a um contexto de exceção contendo informações relevantes desde o ponto em que a exceção foi detetada, particularmente a HttpRequestMessage, o HttpRequestContext, a exceção lançada e a fonte da exceção (detalhes abaixo).
Princípios de estrutura
- Sem alterações radicais Como esta funcionalidade está a ser adicionada numa atualização menor, uma restrição importante que afeta a solução é que não haja alterações rupturas, nem no comportamento nem nos contratos de tipo. Esta restrição excluiu alguma limpeza que gostaríamos de ter feito, em termos de blocos de captura existentes que transformavam exceções em 500 respostas. Esta limpeza adicional é algo que poderemos considerar para uma libertação maior subsequente.
- Manutenção da consistência com as construções da Web API O pipeline de filtros da Web API é uma excelente forma de lidar com questões transversais, com a flexibilidade de aplicar a lógica num âmbito específico de ação, controlador ou global. Os filtros, incluindo os filtros de exceção, têm sempre contextos de ação e controlador, mesmo quando registados no âmbito global. Esse contrato faz sentido para filtros, mas significa que filtros de exceção, mesmo com âmbito global, não são adequados para alguns casos de tratamento de exceções, como exceções de manipuladores de mensagens, onde não existe contexto de ação ou controlador. Se quisermos usar o âmbito flexível proporcionado pelos filtros para o tratamento de exceções, ainda precisamos de filtros de exceção. Mas se precisarmos de gerir exceções fora do contexto do controlador, também precisamos de uma construção separada para o tratamento global completo de erros (algo sem as restrições do contexto do controlador e do contexto de ação).
Quando usar
- Os registos de exceções são a solução para ver todas as exceções não tratadas detetadas pela Web API.
- Os tratadores de exceções são a solução para personalizar todas as respostas possíveis a exceções não tratadas detetadas pela Web API.
- Os filtros de exceções são a solução mais fácil para processar as exceções não tratadas de subconjunto relacionadas com uma ação ou controlador específico.
Detalhes de Serviço
As interfaces de serviço do registo e gestão de exceções são métodos simples assíncronos que utilizam os respetivos 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. Sobrepor os métodos core (sync ou assync) é tudo o que é necessário para registar ou gerir nos horários recomendados. Para registos, a ExceptionLogger classe base garante que o método de registo central só é chamado uma vez para cada exceção (mesmo que mais tarde se propague mais acima na pilha de chamadas e seja apanhado novamente). A classe base ExceptionHandler chamará o método central de tratamento somente para exceções no topo da pilha de execução, ignorando blocos de captura aninhados legados. (Versões simplificadas destas classes base encontram-se no apêndice abaixo.) Tanto IExceptionLogger como IExceptionHandler recebem informação sobre a exceção através 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 o framework chama um logger de exceções ou um gerenciador de exceções, fornecerá sempre o Exception e o Request. À exceção dos testes unitários, fornecerá sempre um RequestContext. Raramente fornece um ControllerContext e ActionContext (apenas quando invocado a partir do bloco de captura para filtros de exceção). Raramente fornece um Response(apenas em certos casos de IIS, quando estou a meio de tentar escrever a resposta). Note que, como algumas destas propriedades podem ser null, cabe ao consumidor verificar null antes de aceder aos membros da classe de exceção.
CatchBlock é uma string que indica qual bloco de captura recebeu a exceção. As cadeias de caracteres do bloco de captura são as seguintes:
HttpServer (método SendAsync)
HttpControllerDispatcher (método SendAsync)
HttpBatchHandler (método SendAsync)
IExceptionFilter (Processamento do pipeline de filtro de exceções no ExecuteAsync pelo ApiController)
Servidor OWIN
- HttpMessageHandlerAdapter.BufferResponseContentAsync (para armazenamento em buffer da saída)
- HttpMessageHandlerAdapter.CopyResponseContentAsync (para saída em streaming)
Alojamento web:
- HttpControllerHandler.WriteBufferedResponseContentAsync (para armazenamento em buffer de saída)
- HttpControllerHandler.WriteStreamedResponseContentAsync (para saída em streaming)
- HttpControllerHandler.WriteErrorResponseContentAsync (para falhas na recuperação de erros em modo de saída com buffer)
A lista de cadeias de blocos de captura também está disponível através de propriedades estáticas apenas de leitura. (A cadeia de blocos de captura principais está nos ExceptionCatchBlocks estáticos; o restante aparece numa classe estática para OWIN e noutra para a hospedagem web).
IsTopLevelCatchBlock é útil para seguir o padrão recomendado de tratar exceções apenas no topo da pilha de chamadas. Em vez de transformar exceções em 500 respostas em qualquer lugar onde ocorra um bloco de captura aninhado, um tratador de exceções pode deixar que as exceções se propaguem até que estejam prestes a ser vistas pelo hospedeiro.
Além do ExceptionContext, um logger obtém mais uma informação através do ExceptionLoggerContext completo:
public class ExceptionLoggerContext
{
public ExceptionContext ExceptionContext { get; set; }
public bool CanBeHandled { get; set; }
}
A segunda propriedade, CanBeHandled, permite que um logger identifique uma exceção que não pode ser tratada. Quando a ligação está prestes a ser abortada e nenhuma nova mensagem de resposta pode ser enviada, os loggers serão chamados, mas o handler não será chamado, e os loggers podem identificar este cenário a partir desta propriedade.
Além do ExceptionContext, um handler obtém mais uma propriedade que pode definir na totalidade ExceptionHandlerContext para tratar a exceção:
public class ExceptionHandlerContext
{
public ExceptionContext ExceptionContext { get; set; }
public IHttpActionResult Result { get; set; }
}
Um mecanismo de tratamento de exceções indica que tratou uma exceção ao configurar a propriedade Result para um resultado de uma ação (por exemplo, um ExceptionResult, InternalServerErrorResult, StatusCodeResult ou um resultado personalizado). Se a Result propriedade for nula, a exceção não é tratada e a exceção original será relançada.
Para exceções no topo da stack de chamadas, demos um passo extra para garantir que a resposta é adequada para os utilizadores da API. Se a exceção se propagar até ao host, o utilizador verá o Ecrã Amarelo da Morte ou outra resposta fornecida pelo host, que normalmente é HTML e não é uma resposta de erro de API apropriada. Nestes casos, o Resultado começa por ser não-nulo, e só se um manipulador de exceções personalizado o definir explicitamente de volta para null (não tratado), a exceção se propagará para o host. Definir Result para null nestes casos pode ser útil para dois cenários:
- API Web alojada pelo OWIN com middleware personalizado de gestão de exceções registado antes/fora da Web API.
- Depuração local através de um navegador, onde o ecrã amarelo de morte serve como uma resposta útil e eficaz para uma exceção não tratada.
Tanto para loggers de exceções quanto para handlers de exceções, não fazemos nada para recuperar caso o próprio logger ou handler lance uma exceção. (Para além de deixar a exceção propagar-se, deixe feedback no final desta página se tiver uma abordagem melhor.) O contrato para registos e manipuladores de exceções é que não devem permitir que exceções se propaguem até aos seus invocadores, caso contrário, a exceção propagar-se-á, muitas vezes até ao host, resultando num erro HTML (como no ASP.NET, o ecrã amarelo) a ser enviado de volta ao cliente (o que normalmente não é a opção preferida para chamadas de API que esperam JSON ou XML).
Exemplos
Registador de Exceções de Rastreio
O registo de exceções abaixo envia dados de exceção para fontes Trace configuradas (incluindo a janela de saída Debug no Visual Studio).
class TraceExceptionLogger : ExceptionLogger
{
public override void LogCore(ExceptionLoggerContext context)
{
Trace.TraceError(context.ExceptionContext.Exception.ToString());
}
}
Manipulador Personalizado de Exceções de Mensagens de Erro
O gestor de exceções abaixo produz uma resposta de erro personalizada aos clientes, incluindo um endereço de email para contactar 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);
}
}
}
Registar Filtros de Exceção
Se usar o modelo de projeto "ASP.NET MVC 4 Web Application" para criar o seu projeto, coloque o seu código de configuração da Web API 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;
}
}