Как зарегистрировать ВСЕ исключения в глобальном масштабе для приложения C # MVC4 WebAPI?

175

Задний план

Я разрабатываю сервисный уровень API для клиента, и меня попросили перехватывать и регистрировать все ошибки в глобальном масштабе.

Таким образом, хотя что-то вроде неизвестной конечной точки (или действия) легко обрабатывается с помощью ELMAH или путем добавления чего-то вроде этого в Global.asax:

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

, , Необработанные ошибки, не связанные с маршрутизацией, не регистрируются. Например:

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

Я также попытался установить [HandleError]атрибут глобально, зарегистрировав этот фильтр:

filters.Add(new HandleErrorAttribute());

Но это также не регистрирует все ошибки.

Проблема / Вопрос

Как перехватить ошибки, подобные той, которая генерируется при вызове /testвыше, чтобы я мог их регистрировать? Кажется, что этот ответ должен быть очевидным, но я перепробовал все, что только могу придумать.

В идеале я хочу добавить в журнал ошибок некоторые вещи, такие как IP-адрес запрашивающего пользователя, дату, время и т. Д. Я также хочу, чтобы сотрудники службы поддержки могли по электронной почте автоматически при обнаружении ошибки. Все это я могу сделать, если только я могу перехватить эти ошибки, когда они случаются!

ПОСТАНОВИЛИ!

Благодаря Дарину Димитрову, чей ответ я принял, я понял это. WebAPI не обрабатывает ошибки так же, как обычный контроллер MVC.

Вот что сработало:

1) Добавьте пользовательский фильтр в ваше пространство имен:

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) Теперь зарегистрируйте фильтр глобально в классе 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());
     }
}

ИЛИ вы можете пропустить регистрацию и просто украсить отдельный контроллер с [ExceptionHandling]атрибутом.

Мэтт Кашатт
источник
У меня та же проблема. Необработанные исключения хорошо попадают в атрибут фильтра исключений, но когда я генерирую новое исключение, оно не попадает в атрибут фильтра исключений, есть идеи относительно этого?
daveBM
1
Неизвестные вызовы контроллера API, такие как ошибки myhost / api / undefinedapicontroller , все еще не обнаружены. Код фильтра Application_error и Exception не выполняется. Как их тоже поймать?
Андрус
1
Глобальная обработка ошибок была добавлена ​​в WebAPI v2.1. Смотрите мой ответ здесь: stackoverflow.com/questions/17449400/…
DarrellNorton
1
Это не будет отлавливать ошибки при некоторых обстоятельствах, таких как «ресурс не найден» или ошибки в конструкторе контроллера. См. Здесь: aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/Elmah/…
Джордан Моррис
Привет, @Matt. Вы написали ответ как часть вопроса, но это не лучшая практика в SO. Здесь ответы должны быть отделены от вопроса. Не могли бы вы написать это как отдельный ответ (вы можете использовать синюю кнопку «Ответить на свой вопрос» внизу).
sashoalm

Ответы:

56

Если ваш веб-API размещен внутри приложения ASP.NET, Application_Errorсобытие будет вызываться для всех необработанных исключений в вашем коде, в том числе в тестовом действии, которое вы показали. Поэтому все, что вам нужно сделать, это обработать это исключение внутри события Application_Error. В показанном вами примере кода вы обрабатываете только исключение типа, HttpExceptionчто, очевидно, не относится к Convert.ToInt32("a")коду. Поэтому убедитесь, что вы регистрируете и обрабатываете все исключения там:

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

Обработка исключений в Web API может выполняться на разных уровнях. Вот detailed articleобъяснение различных возможностей:

  • атрибут фильтра пользовательских исключений, который может быть зарегистрирован как глобальный фильтр исключений

    [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"
            });
        }
    }
  • пользователь, выполняющий пользовательские действия

    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;
        }
    }
Дарин димитров
источник
Хотелось бы, чтобы все было так просто, но ошибка все еще не обнаруживается. Я обновил вопрос, чтобы избежать путаницы. Спасибо.
Мэтт Кашатт
@MatthewPatrickCashatt, если это исключение не попадает в Application_Errorсобытие, это означает, что какой-то другой код потребляет его раньше. Например, у вас могут быть некоторые пользовательские атрибуты HandleErrorAttributes, пользовательские модули, ... Есть десятки других мест, где исключения могут быть перехвачены и обработаны. Но лучшее место для этого - событие Application_Error, потому что на этом все необработанные исключения будут заканчиваться.
Дарин Димитров
Еще раз спасибо, но, несмотря ни на что, /testпример не получил успеха. Я поставил точку останова на первой строке ( Exception unhandledException = . . .), но не могу достичь этой точки останова в /testсценарии. Однако, если я введу фиктивный URL, точка останова будет достигнута.
Мэтт Кашатт
1
@ MatthewPatrickCashatt, вы совершенно правы. Application_ErrorСобытие не является правильным местом для обработки исключений для Web API , поскольку он не будет срабатывать во всех случаях. Я нашел очень подробную статью, объясняющую различные возможности для достижения этой цели: weblogs.asp.net/fredriknormen/archive/2012/06/11/…
Дарин Димитров
1
@Darin Dimitrov Неизвестные вызовы контроллера API, такие как ошибки myhost / api / undefinedapi , по-прежнему не отслеживаются. Код фильтра Application_error и Exception не выполняется. Как их тоже поймать?
Андрус
79

Как дополнение к предыдущим ответам.

Вчера был официально выпущен ASP.NET Web API 2.1 .
Это дает еще одну возможность обрабатывать исключения во всем мире.
Детали приведены в образце .

Вкратце, вы добавляете глобальные регистраторы исключений и / или глобальный обработчик исключений (только один).
Вы добавляете их в конфигурацию:

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

И их реализация:

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);
  }
}
Владимир
источник
2
Это сработало отлично. Я регистрирую и обрабатываю одновременно (потому что я получаю logID и передаю его обратно, чтобы пользователь мог добавить комментарий), поэтому я устанавливаю Result для нового ResponseMessageResult. Это беспокоило меня некоторое время, спасибо.
Бретт
8

Зачем отбрасывать и т. Д.? Это работает, и сервис вернет статус 500 и т. Д.

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);
    }
}
Андерс
источник
2

Вы думали о том, чтобы сделать что-то вроде фильтра действия ошибки дескриптора, как

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

Вы также можете создать собственную версию [HandleError] с которой вы можете написать информацию об ошибке и все другие детали для входа

ХОЛОДНЫЙ СКАЗ
источник
Спасибо, но у меня уже есть этот набор глобально. Это создает ту же проблему, что и выше, не все ошибки регистрируются.
Мэтт Кашатт
1

Оберните все это в попытку / поймайте и зарегистрируйте необработанное исключение, а затем передайте его. Если нет лучшего встроенного способа сделать это.

Вот ссылка Catch All (обработано или необработано) Исключения

(редактировать: о, API)

Тим
источник
На всякий случай ему тоже нужно будет отбросить исключение.
DigCamara
@DigCamara Извините, это то, что я имел в виду под передачей. бросить; должен справиться с этим. Первоначально я сказал «решите, выходить или перезагрузить», потом понял, что он сказал, что это API. В этом случае лучше всего позволить приложению решить, что оно хочет сделать, передав его.
Тим
1
Это плохой ответ, потому что это приведет к загрузке дублированного кода в каждом действии.
Янски