Глобальная обработка ошибок в веб-API ASP.NET 2

Дэвид Мэтсон (David Matson), Рик Андерсон (Rick Anderson)

В этом разделе представлен обзор глобальной обработки ошибок в веб-API ASP.NET 2 для ASP.NET 4.x. Сегодня в веб-API нет простого способа глобального ведения журнала или обработки ошибок. Некоторые необработанных исключений могут обрабатываться с помощью фильтров исключений, но существует ряд случаев, которые не могут обрабатываться фильтрами исключений. Пример:

  1. Исключения выброшены из конструкторов контроллеров.
  2. Исключения выброшены из обработчиков сообщений.
  3. Исключения выброшены при маршрутизации.
  4. Исключения выброшены при сериализации содержимого ответа.

Мы хотим предоставить простой и согласованный способ ведения журнала и обработки (где это возможно) этих исключений.

Существует два основных случая обработки исключений: случай, когда мы можем отправить ответ об ошибке, и случай, когда все, что мы можем сделать, — это записать исключение в журнал. Примером последнего случая является исключение в середине содержимого ответа потоковой передачи; В этом случае слишком поздно отправлять новое ответное сообщение, так как код состояния, заголовки и частичное содержимое уже прошли по сети, поэтому мы просто прервем подключение. Несмотря на то, что исключение не может быть обработано для создания нового ответного сообщения, мы по-прежнему поддерживаем ведение журнала исключения. В случаях, когда мы можем обнаружить ошибку, мы можем вернуть соответствующий ответ об ошибке, как показано ниже.

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

Существующие параметры

В дополнение к фильтрам исключенийобработчики сообщений можно использовать сегодня для наблюдения за всеми 500-уровневными ответами, но действовать с этими ответами сложно, так как у них нет контекста об исходной ошибке. Обработчики сообщений также имеют некоторые из тех же ограничений, что и фильтры исключений в отношении случаев, которые они могут обрабатывать. Хотя веб-API имеет инфраструктуру трассировки, которая фиксирует условия ошибок, инфраструктура трассировки предназначена для диагностика целей и не предназначена и не подходит для работы в рабочих средах. Глобальная обработка исключений и ведение журнала должны быть службами, которые могут выполняться во время рабочей среды и быть подключены к существующим решениям мониторинга (например, ELMAH).

Общие сведения о решении

Мы предоставляем две новые службы, заменяемые пользователем, IExceptionLogger и IExceptionHandler, для регистрации и обработки необработанных исключений. Службы очень похожи, с двумя main различиями:

  1. Мы поддерживаем регистрацию нескольких средств ведения журнала исключений, но только одного обработчика исключений.
  2. Средства ведения журнала исключений всегда вызываются, даже если мы собираемся прервать подключение. Обработчики исключений вызываются только в том случае, если мы все еще можем выбрать ответные сообщения для отправки.

Обе службы предоставляют доступ к контексту исключения, содержашему соответствующую информацию с точки, где было обнаружено исключение, в частности HttpRequestMessage, HttpRequestContext, созданное исключение и источник исключения (подробности ниже).

Принципы проектирования

  1. Без критических изменений Так как эта функция добавляется в дополнительном выпуске, одним из важных ограничений, влияющих на решение, является отсутствие критических изменений в типах контрактов или в поведении. Это ограничение исключило некоторые действия по очистке, которые мы хотели бы выполнить с точки зрения существующих блоков catch, превратив исключения в 500 ответов. Эту дополнительную очистку можно рассмотреть для последующего основного выпуска.
  2. Обеспечение согласованности с помощью конструкций веб-API Конвейер фильтрации веб-API — это отличный способ решения сквозных проблем с гибкостью применения логики на конкретных действиях, контроллерах или глобальных область. Фильтры, включая фильтры исключений, всегда имеют контексты действий и контроллера, даже если они зарегистрированы на глобальном область. Этот контракт имеет смысл для фильтров, но это означает, что фильтры исключений, даже глобальной области, не подходят для некоторых случаев обработки исключений, таких как исключения из обработчиков сообщений, где не существует контекста действия или контроллера. Если мы хотим использовать гибкую область, предоставленную фильтрами для обработки исключений, нам по-прежнему нужны фильтры исключений. Но если нам нужно обрабатывать исключение вне контекста контроллера, нам также нужна отдельная конструкция для полной глобальной обработки ошибок (без ограничений контекста контроллера и контекста действия).

Назначение

  • Средства ведения журнала исключений — это решение для просмотра всех необработанных исключений, перехватанных веб-API.
  • Обработчики исключений — это решение для настройки всех возможных ответов на необработанных исключений, перехватанных веб-API.
  • Фильтры исключений — это самое простое решение для обработки подмножества необработанных исключений, связанных с определенным действием или контроллером.

Сведения о службе

Интерфейсы службы ведения журнала исключений и обработчика — это простые асинхронные методы, принимающие соответствующие контексты:

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

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

Мы также предоставляем базовые классы для обоих этих интерфейсов. Переопределение основных методов (синхронных или асинхронных) — это все, что требуется для регистрации или обработки в рекомендуемое время. Для ведения журнала базовый ExceptionLogger класс гарантирует, что основной метод ведения журнала вызывается только один раз для каждого исключения (даже если позже он распространяется дальше вверх по стеку вызовов и перехватывается снова). Базовый ExceptionHandler класс будет вызывать метод обработки ядра только для исключений в верхней части стека вызовов, игнорируя устаревшие вложенные блоки catch. (Упрощенные версии этих базовых классов приведены в приложении ниже.) И IExceptionLogger получают IExceptionHandler сведения об исключении через 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; }
}

Когда платформа вызывает средство ведения журнала исключений или обработчик исключений, она всегда предоставляет Exception и Request. За исключением модульного тестирования, он также всегда предоставляет RequestContext. Он редко предоставляет ControllerContext и ActionContext (только при вызове из блока catch для фильтров исключений). Он очень редко предоставляет Response(только в некоторых случаях IIS, когда в середине пытается написать ответ). Обратите внимание, что, поскольку некоторые из этих свойств могут бытьnull, потребитель должен проверка перед null доступом к членам класса исключения.CatchBlock — это строка, указывающая, какой блок catch видел исключение. Ниже приведены строки блока catch.

  • HttpServer (метод SendAsync)

  • HttpControllerDispatcher (метод SendAsync)

  • HttpBatchHandler (метод SendAsync)

  • IExceptionFilter (обработка ApiController конвейера фильтра исключений в ExecuteAsync)

  • Узел OWIN:

    • HttpMessageHandlerAdapter.BufferResponseContentAsync (для буферизации выходных данных)
    • HttpMessageHandlerAdapter.CopyResponseContentAsync (для потокового вывода)
  • Веб-узел:

    • HttpControllerHandler.WriteBufferedResponseContentAsync (для буферизации выходных данных)
    • HttpControllerHandler.WriteStreamedResponseContentAsync (для потокового вывода)
    • HttpControllerHandler.WriteErrorResponseContentAsync (для сбоев при восстановлении ошибок в режиме буферизованного вывода)

Список строк блока catch также доступен через статические свойства только для чтения. (Строки блока core catch находятся в статическом блоке ExceptionCatchBlocks; остаток отображается в одном статическом классе для OWIN и веб-узла).IsTopLevelCatchBlock рекомендуется использовать для обработки исключений только в верхней части стека вызовов. Вместо того, чтобы превращать исключения в 500 ответов в любом месте вложенного блока catch, обработчик исключений может разрешить распространение исключений до тех пор, пока они не будут видны узлу.

В дополнение к ExceptionContextсредство ведения журнала получает еще один фрагмент информации через полный ExceptionLoggerContext:

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

Второе свойство, CanBeHandled, позволяет средству ведения журнала идентифицировать исключение, которое не может быть обработано. Когда подключение будет прервано и новое ответное сообщение не может быть отправлено, будут вызваны средства ведения журнала, но обработчик не будет вызван, и средства ведения журнала могут определить этот сценарий по этому свойству.

В дополнение к ExceptionContextобработчик получает еще одно свойство, которое он может задать для полной ExceptionHandlerContext обработки исключения:

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

Обработчик исключений указывает, что он обработал исключение, задав Result для свойства результат действия (например, ExceptionResult, InternalServerErrorResult, StatusCodeResult или пользовательский результат). Result Если свойство имеет значение NULL, исключение необработано, и исходное исключение будет создано повторно.

Для исключений в верхней части стека вызовов мы предприняли дополнительный шаг, чтобы убедиться, что ответ подходит для вызывающих объектов API. Если исключение распространяется на узел, вызывающий объект увидит желтый экран смерти или ответ другого узла, который обычно является HTML и обычно не является соответствующим ответом об ошибке API. В таких случаях результат начинается с ненулевого значения, и только если пользовательский обработчик исключений явно задает ему null значение (необработанное), исключение будет распространяться на узел. Установка значения Resultnull в таких случаях может быть полезна в двух сценариях:

  1. Веб-API, размещенный в OWIN, с пользовательским ПО промежуточного слоя для обработки исключений, зарегистрированным до или за пределами веб-API.
  2. Локальная отладка через браузер, где желтый экран смерти на самом деле является полезным ответом на необработанное исключение.

Как для средств ведения журнала исключений, так и для обработчиков исключений мы не делаем никаких действий для восстановления, если средство ведения журнала или обработчик сам создает исключение. (Кроме того, чтобы разрешить распространение исключения, оставьте отзыв в нижней части этой страницы, если у вас есть лучший подход.) Контракт для средств ведения журнала исключений и обработчиков заключается в том, что они не должны позволять исключениям распространяться на их вызывающие объекты; в противном случае исключение будет просто распространяться, часто на весь путь к узлу, что приводит к ошибке HTML (например, ASP. Желтый экран NET) отправляется обратно клиенту (что обычно не является предпочтительным вариантом для вызывающих сторон API, ожидающих JSON или XML).

Примеры

Средство ведения журнала исключений трассировки

Приведенное ниже средство ведения журнала исключений отправляет данные исключений в настроенные источники трассировки (включая окно вывода отладки в Visual Studio).

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

Настраиваемый обработчик исключений сообщений об ошибках

Обработчик исключений, приведенный ниже, создает настраиваемый ответ об ошибке для клиентов, включая адрес электронной почты для обращения в службу поддержки.

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

Регистрация фильтров исключений

Если вы используете шаблон проекта "веб-приложение ASP.NET MVC 4" для создания проекта, поместите код конфигурации веб-API в WebApiConfig класс в папку App_Start :

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

        // Other configuration code...
    }
}

Приложение. Сведения о базовом классе

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