¿Cómo registro TODAS las excepciones a nivel mundial para una aplicación WebAPI C # MVC4?

175

Antecedentes

Estoy desarrollando una capa de servicio API para un cliente y se me ha pedido que capture y registre todos los errores a nivel mundial.

Entonces, mientras que algo como un punto final desconocido (o acción) se maneja fácilmente usando ELMAH o agregando algo como esto a Global.asax:

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

. . Los errores no controlados que no están relacionados con el enrutamiento no se registran. Por ejemplo:

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

También he intentado establecer el [HandleError]atributo globalmente al registrar este filtro:

filters.Add(new HandleErrorAttribute());

Pero eso tampoco registra todos los errores.

Problema / Pregunta

¿Cómo intercepto errores como el generado al llamar /testarriba para poder registrarlos? Parece que esta respuesta debería ser obvia, pero he intentado todo lo que puedo pensar hasta ahora.

Idealmente, quiero agregar algunas cosas al registro de errores, como la dirección IP del usuario solicitante, la fecha, la hora, etc. También quiero poder enviar un correo electrónico al personal de soporte automáticamente cuando se encuentre un error. ¡Todo esto lo puedo hacer si solo puedo interceptar estos errores cuando ocurren!

¡RESUELTO!

Gracias a Darin Dimitrov, cuya respuesta acepté, me di cuenta de esto. WebAPI no maneja los errores de la misma manera que un controlador MVC normal.

Esto es lo que funcionó:

1) Agregue un filtro personalizado a su espacio de nombres:

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) Ahora registre el filtro globalmente en la clase 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());
     }
}

O puede omitir el registro y simplemente decorar un solo controlador con el [ExceptionHandling]atributo.

Matt Cashatt
fuente
Tengo el mismo problema. Las excepciones no controladas quedan atrapadas en el atributo de filtro de excepción, pero cuando lanzo una nueva excepción no queda atrapada en el atributo de filtro de excepción, ¿alguna idea con respecto a eso?
daveBM
1
Las llamadas desconocidas del controlador api como errores myhost / api / undefinedapicontroller todavía no se detectan. El código de filtro Application_error and Exception no se ejecuta. ¿Cómo atraparlos también?
Andrus
1
El manejo de errores globales se agregó a WebAPI v2.1. Vea mi respuesta aquí: stackoverflow.com/questions/17449400/…
DarrellNorton
1
Esto no detectará errores en algunas circunstancias, como "recurso no encontrado" o errores en un constructor de controlador. Consulte aquí: aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/Elmah/…
Jordan Morris
Hola Matt. Ha escrito la respuesta como parte de la pregunta, pero esta no es una práctica recomendada en SO. Aquí las respuestas deben estar separadas de la pregunta. ¿Podría escribir eso como una respuesta por separado (puede usar el botón azul "Responda su propia pregunta" en la parte inferior).
sashoalm

Respuestas:

56

Si su API web está alojada dentro de una aplicación ASP.NET, Application_Errorse llamará al evento para todas las excepciones no controladas en su código, incluida la de la acción de prueba que ha mostrado. Entonces, todo lo que tiene que hacer es manejar esta excepción dentro del evento Application_Error. En el código de muestra que ha mostrado, solo está manejando una excepción de tipo, HttpExceptionque obviamente no es el caso con el Convert.ToInt32("a")código. Así que asegúrese de iniciar sesión y manejar todas las excepciones allí:

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
        }
    }
}

El manejo de excepciones en la API web podría hacerse en varios niveles. Aquí hay una detailed articleexplicación de las diferentes posibilidades:

  • atributo de filtro de excepción personalizado que podría registrarse como un filtro de excepción 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 acción 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
fuente
Desearía que fuera así de simple, pero el error aún no se detecta. He actualizado la pregunta para evitar confusiones. Gracias.
Matt Cashatt
@MatthewPatrickCashatt, si esta excepción no se detecta en el Application_Errorevento, esto significa que algún otro código la está consumiendo antes. Por ejemplo, puede tener algunos atributos HandleErrorAttributes personalizados, módulos personalizados, ... Hay miles de millones de otros lugares donde se pueden detectar y manejar excepciones. Pero el mejor lugar para hacerlo es el evento Application_Error, porque ahí es donde terminarán todas las excepciones no controladas.
Darin Dimitrov
Gracias de nuevo, pero pase lo que pase, el /testejemplo no se ve afectado. He puesto un punto de interrupción en la primera línea ( Exception unhandledException = . . .) pero no puedo alcanzar ese punto de interrupción en el /testescenario. Sin embargo, si pongo una URL falsa, se alcanza el punto de interrupción.
Matt Cashatt
1
@MatthewPatrickCashatt, tienes toda la razón. El Application_Errorevento no es el lugar correcto para manejar excepciones para la API web porque no se activará en todos los casos. He encontrado un artículo muy detallado que explica las diversas posibilidades para lograrlo: weblogs.asp.net/fredriknormen/archive/2012/06/11/…
Darin Dimitrov
1
@Darin Dimitrov Las llamadas de controlador de API desconocidas como errores de myhost / api / undefinedapi todavía no se detectan. El código de filtro Application_error and Exception no se ejecuta. ¿Cómo atraparlos también?
Andrus
79

Como una adición a las respuestas anteriores.

Ayer, se lanzó oficialmente ASP.NET Web API 2.1 .
Ofrece otra oportunidad para manejar excepciones a nivel mundial.
Los detalles se dan en la muestra .

Brevemente, agrega registradores de excepciones globales y / o manejador de excepciones globales (solo uno).
Los agrega a la configuración:

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());
}

Y su realización:

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
fuente
2
Esto funcionó perfectamente. Registro y manejo simultáneamente (porque obtengo el logID y lo devuelvo para que el usuario pueda agregar comentarios), por lo que estoy configurando Result en un nuevo ResponseMessageResult. Esto me ha estado molestando por un tiempo, gracias.
Brett
8

¿Por qué volver a tirar, etc.? Esto funciona y hará que el servicio devuelva el estado 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
fuente
2

¿Has pensado en hacer algo como un filtro de acción de error de manejo como

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

También puede crear una versión personalizada [HandleError]con la que puede escribir información de error y todos los demás detalles para iniciar sesión

TOLD FRÍO
fuente
Gracias, pero ya lo tengo a nivel mundial. Presenta el mismo problema que el anterior, no todos los errores se registran.
Matt Cashatt
1

Envuelva todo en un try / catch y registre la excepción no controlada, luego páselo. A menos que haya una mejor forma integrada de hacerlo.

Aquí hay una referencia Catch All (manejado o no manejado) Excepciones

(editar: oh API)

Tim
fuente
Por si acaso, también necesitaría volver a lanzar la excepción.
DigCamara
@DigCamara Lo siento, eso es lo que quise decir con transmitirlo. lanzar; debería manejar eso. Originalmente dije "decide si salir o volver a cargar", luego me di cuenta de que había dicho que era una API. En ese caso, lo mejor es dejar que la aplicación decida qué quiere hacer al transmitirla.
Tim
1
Esta es una mala respuesta porque dará como resultado un montón de código duplicado en cada acción.
Jansky