Como registrar TODAS as exceções globalmente para um aplicativo C # MVC4 WebAPI?

175

fundo

Estou desenvolvendo uma camada de serviço de API para um cliente e fui solicitado a capturar e registrar todos os erros globalmente.

Portanto, enquanto algo como um terminal (ou ação) desconhecido é facilmente manipulado usando o ELMAH ou adicionando algo como isto ao Global.asax:

protected void Application_Error()
{
     Exception unhandledException = Server.GetLastError();
     //do more stuff
}

. . Os erros não manipulados que não estão relacionados ao roteamento não são registrados. Por exemplo:

public class ReportController : ApiController
{
    public int test()
    {
        var foo = Convert.ToInt32("a");//Will throw error but isn't logged!!
        return foo;
    }
}

Eu também tentei definir o [HandleError]atributo globalmente registrando este filtro:

filters.Add(new HandleErrorAttribute());

Mas isso também não registra todos os erros.

Problema / Pergunta

Como intercepto erros como o gerado pela chamada /testacima para que eu possa registrá-los? Parece que essa resposta deve ser óbvia, mas eu tentei tudo o que consigo pensar até agora.

Idealmente, quero adicionar algumas coisas ao log de erros, como o endereço IP do usuário solicitante, data, hora e assim por diante. Também quero poder enviar um e-mail para a equipe de suporte automaticamente quando um erro for encontrado. Tudo isso eu posso fazer se eu puder interceptar esses erros quando eles acontecerem!

RESOLVIDO!

Graças a Darin Dimitrov, cuja resposta eu aceitei, resolvi isso. O WebAPI não trata erros da mesma maneira que um controlador MVC comum.

Aqui está o que funcionou:

1) Adicione um filtro personalizado ao seu espaço para nome:

public class ExceptionHandlingAttribute : ExceptionFilterAttribute
{
    public override void OnException(HttpActionExecutedContext context)
    {
        if (context.Exception is BusinessException)
        {
            throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
            {
                Content = new StringContent(context.Exception.Message),
                ReasonPhrase = "Exception"
            });

        }

        //Log Critical errors
        Debug.WriteLine(context.Exception);

        throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
        {
            Content = new StringContent("An error occurred, please try again or contact the administrator."),
            ReasonPhrase = "Critical Exception"
        });
    }
}

2) Agora registre o filtro globalmente na classe WebApiConfig :

public static class WebApiConfig
{
     public static void Register(HttpConfiguration config)
     {
         config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{action}/{id}", new { id = RouteParameter.Optional });
         config.Filters.Add(new ExceptionHandlingAttribute());
     }
}

OU você pode pular o registro e apenas decorar um único controlador com o [ExceptionHandling]atributo

Matt Cashatt
fonte
Eu tenho o mesmo problema. Exceções não tratadas são capturadas no atributo de filtro de exceção, mas quando ligo uma nova exceção, ela não é capturada no atributo de filtro de exceção, alguma idéia sobre isso?
DaveBM
1
Chamadas desconhecidas do controlador da API, como erros myhost / api / undefinedapicontroller , ainda não foram capturados. O código do filtro Application_error e Exception não é executado. Como pegá-los também?
Andrus 26/11
1
O tratamento global de erros foi adicionado ao WebAPI v2.1. Veja minha resposta aqui: stackoverflow.com/questions/17449400/…
DarrellNorton
1
Isso não detectará erros em algumas circunstâncias, como "recurso não encontrado" ou erros em um construtor de controlador. Consulte aqui: aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/Elmah/...
Jordan Morris
Olá, @Matt. Você escreveu a resposta como parte da pergunta, mas essa não é uma prática recomendada no SO. Aqui as respostas devem ser separadas da pergunta. Você poderia escrever isso como uma resposta separada (você pode usar o botão azul "Responda sua própria pergunta" na parte inferior).
Sashoalm

Respostas:

56

Se sua API da web estiver hospedada em um aplicativo ASP.NET, o Application_Errorevento será chamado para todas as exceções não tratadas em seu código, incluindo a ação de teste que você mostrou. Portanto, tudo o que você precisa fazer é lidar com essa exceção dentro do evento Application_Error. No código de exemplo que você mostrou, você está manipulando apenas a exceção do tipo HttpExceptionque obviamente não é o caso do Convert.ToInt32("a")código. Portanto, certifique-se de registrar e manipular todas as exceções nele:

protected void Application_Error()
{
    Exception unhandledException = Server.GetLastError();
    HttpException httpException = unhandledException as HttpException;
    if (httpException == null)
    {
        Exception innerException = unhandledException.InnerException;
        httpException = innerException as HttpException;
    }

    if (httpException != null)
    {
        int httpCode = httpException.GetHttpCode();
        switch (httpCode)
        {
            case (int)HttpStatusCode.Unauthorized:
                Response.Redirect("/Http/Error401");
                break;

            // TODO: don't forget that here you have many other status codes to test 
            // and handle in addition to 401.
        }
        else
        {
            // It was not an HttpException. This will be executed for your test action.
            // Here you should log and handle this case. Use the unhandledException instance here
        }
    }
}

O tratamento de exceções na API da Web pode ser feito em vários níveis. Aqui está uma detailed articleexplicação das diferentes possibilidades:

  • atributo de filtro de exceção personalizado que pode ser registrado como um filtro de exceção global

    [AttributeUsage(AttributeTargets.All)]
    public class ExceptionHandlingAttribute : ExceptionFilterAttribute
    {
        public override void OnException(HttpActionExecutedContext context)
        {
            if (context.Exception is BusinessException)
            {
                throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
                {
                    Content = new StringContent(context.Exception.Message),
                    ReasonPhrase = "Exception"
                });
            }
    
            //Log Critical errors
            Debug.WriteLine(context.Exception);
    
            throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
            {
                Content = new StringContent("An error occurred, please try again or contact the administrator."),
                ReasonPhrase = "Critical Exception"
            });
        }
    }
  • invocador de ação personalizada

    public class MyApiControllerActionInvoker : ApiControllerActionInvoker
    {
        public override Task<HttpResponseMessage> InvokeActionAsync(HttpActionContext actionContext, System.Threading.CancellationToken cancellationToken)
        {
            var result = base.InvokeActionAsync(actionContext, cancellationToken);
    
            if (result.Exception != null && result.Exception.GetBaseException() != null)
            {
                var baseException = result.Exception.GetBaseException();
    
                if (baseException is BusinessException)
                {
                    return Task.Run<HttpResponseMessage>(() => new HttpResponseMessage(HttpStatusCode.InternalServerError)
                    {
                        Content = new StringContent(baseException.Message),
                        ReasonPhrase = "Error"
    
                    });
                }
                else
                {
                    //Log critical error
                    Debug.WriteLine(baseException);
    
                    return Task.Run<HttpResponseMessage>(() => new HttpResponseMessage(HttpStatusCode.InternalServerError)
                    {
                        Content = new StringContent(baseException.Message),
                        ReasonPhrase = "Critical Error"
                    });
                }
            }
    
            return result;
        }
    }
Darin Dimitrov
fonte
Eu gostaria que fosse assim tão simples, mas o erro ainda não foi detectado. Atualizei a pergunta para evitar confusão. Obrigado.
Matt Cashatt
@MatthewPatrickCashatt, se essa exceção não for capturada no Application_Errorevento, isso significa que algum outro código está consumindo-a antes. Por exemplo, você pode ter alguns HandleErrorAttributes, módulos personalizados, ... Existem muitos outros lugares onde as exceções podem ser capturadas e manipuladas. Mas o melhor lugar para fazer isso é o evento Application_Error, porque é onde todas as exceções não tratadas terminam.
Darin Dimitrov
Obrigado novamente, mas não importa o quê, o /testexemplo não é atingido. Coloquei um ponto de interrupção na primeira linha ( Exception unhandledException = . . .), mas não consigo atingi-lo no /testcenário. Se eu inserir um URL falso, o ponto de interrupção será atingido.
Matt Cashatt
1
@MatthewPatrickCashatt, você está completamente certo. O Application_Errorevento não é o local correto para lidar com exceções para a API da Web, porque não será acionado em todos os casos. Eu encontrei um artigo muito detalhado explicando as várias possibilidades de conseguir isso: weblogs.asp.net/fredriknormen/archive/2012/06/11/...
Darin Dimitrov
1
@Darin Dimitrov Chamadas de controlador de API desconhecidas, como os erros myhost / api / undefinedapi , ainda não foram capturados. O código do filtro Application_error e Exception não é executado. Como pegá-los também?
Andrus 26/11
79

Como complemento às respostas anteriores.

Ontem, o ASP.NET Web API 2.1 foi lançado oficialmente .
Oferece outra oportunidade para lidar com exceções globalmente.
Os detalhes são dados na amostra .

Resumidamente, você adiciona registradores de exceções globais e / ou manipulador de exceções globais (apenas um).
Você os adiciona à configuração:

public static void Register(HttpConfiguration config)
{
  config.MapHttpAttributeRoutes();

  // There can be multiple exception loggers.
  // (By default, no exception loggers are registered.)
  config.Services.Add(typeof(IExceptionLogger), new ElmahExceptionLogger());

  // There must be exactly one exception handler.
  // (There is a default one that may be replaced.)
  config.Services.Replace(typeof(IExceptionHandler), new GenericTextExceptionHandler());
}

E sua realização:

public class ElmahExceptionLogger : ExceptionLogger
{
  public override void Log(ExceptionLoggerContext context)
  {
    ...
  }
}

public class GenericTextExceptionHandler : ExceptionHandler
{
  public override void Handle(ExceptionHandlerContext context)
  {
    context.Result = new InternalServerErrorTextPlainResult(
      "An unhandled exception occurred; check the log for more information.",
      Encoding.UTF8,
      context.Request);
  }
}
Vladimir
fonte
2
Isso funcionou perfeitamente. Faço logon e manuseio simultaneamente (porque recebo o logID e o passo de volta para que o usuário possa adicionar comentários); portanto, estou definindo Result como um novo ResponseMessageResult. Isso me incomoda há um tempo, obrigado.
Brett
8

Por que relançar etc? Isso funciona e fará com que o serviço retorne o status 500 etc

public class LogExceptionFilter : ExceptionFilterAttribute
{
    private static readonly ILog log = LogManager.GetLogger(typeof (LogExceptionFilter));

    public override void OnException(HttpActionExecutedContext actionExecutedContext)
    {
        log.Error("Unhandeled Exception", actionExecutedContext.Exception);
        base.OnException(actionExecutedContext);
    }
}
Anders
fonte
2

você já pensou em fazer algo como um filtro de ação de erro de manipulação como

[HandleError]
public class BaseController : Controller {...}

você também pode criar uma versão personalizada [HandleError]com a qual pode gravar informações de erro e todos os outros detalhes para registrar

COLD TOLD
fonte
Obrigado, mas eu já tenho esse cenário globalmente. Ele apresenta o mesmo problema acima, nem todos os erros são registrados.
Matt Cashatt
1

Embrulhe a coisa toda em uma tentativa / captura e registre a exceção não tratada e depois a repasse. A menos que haja uma maneira integrada melhor de fazer isso.

Aqui está uma referência Capturar todas (manipuladas ou não) Exceções

(editar: oh API)

Tim
fonte
Apenas no caso, ele precisaria refazer a exceção também.
precisa saber é o seguinte
@DigCamara Desculpe, é isso que eu quis dizer com passar adiante. lançar; deve lidar com isso. Inicialmente, eu disse: "decida sair ou recarregar", depois percebi que ele havia dito que era uma API. Nesse caso, é melhor deixar que o aplicativo decida o que quer fazer, transmitindo-o.
Tim
1
Essa é uma resposta ruim, pois resultará em cargas de código duplicado em todas as ações.
Jansky