Контроль над процессом обработки ошибок

70

Все примеры, приведенные в предыдущих статьях, полагались на стандартное поведение ASP.NET (и IIS) в отношении ошибок. Для большинства проектов такого стандартного поведения оказывается вполне достаточно. Разумеется, учитывая расширяемость ASP.NET Framework, можно полностью изменить способ обработки ошибок: этой теме посвящена эта статья. В последующих разделах мы рассмотрим различные точки внутри ASP.NET Framework, где можно перехватить контроль и применить собственный процесс.

Обработка ошибки внутри веб-формы

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

Событие Error инициируется, даже если в файле Web.config были отключены специальные ошибки. Свойство IsCustomErrorEnabled объекта HttpContext позволяет выяснить, включены ли специальные ошибки для текущего запроса.

Реагируя на событие Error, можно обеспечить очень точный контроль над тем, как обрабатываются ошибки. В целях демонстрации мы определили новую веб-форму по имени ComponentError.aspx, которая будет использоваться для отображения более специфичной информации об источнике ошибки. Опять-таки, подобное не будет делаться в реальном проекте, т.к. эта информация бесполезна для пользователя, но это нужно для рассматриваемого примера. В примере показано содержимое файла ComponentError.aspx:

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeBehind="ComponentError.aspx.cs" Inherits="ErrorHandling.ComponentError" %>

<!DOCTYPE html>
<html>
<head id="Head1" runat="server">
    <title></title>
</head>
<body>
    <h1>Извините</h1>
    <p>Произошла ошибка в файле
        <span><%: Request["errorSource"] %></span> 
        и мы не смогли корректно обработать ваш запрос.</p>
    <p>Тип ошибки - <span><%: Request["errorType"] %></span></p>
    <p><a href="Default.aspx">Перейти на главную?</a></p>    
</body>
</html>

Для отображения значений параметров errorSource и errorType строки запроса применяются фрагменты кода. Их значения устанавливаются при обработке события Error в файле отделенного кода Default.aspx.cs:

using System;

namespace ErrorHandling
{
    public partial class Default : System.Web.UI.Page
    {
	    // ...

        protected void Page_Error(object sender, EventArgs e)
        {
            if (Context.Error is FormatException)
            {
                Response.Redirect(string.Format
                    ("/ComponentError.aspx?errorSource={0}&errorType={1}",
                    Request.Path,
                    Context.Error.GetType()));
            }
        }
    }
}

Мы определили декларативный метод обработчика события Error и применяем его для перенаправления клиента на веб-форму ComponentError.aspx, если поступившим исключением является FormatException (тип исключения, генерируемого при отправке формы со строковым значением).

Событие Error не определено в элементах управления, а любое исключение, возникающее внутри элемента управления, в результате приводит к инициированию события Error страницы (объекта Page), которая содержит этот элемент управления. По этой причине в файле отделенного кода веб-формы Default.aspx ищутся исключения FormatException, сгенерированные элементом управления SumControl.

Исключение, которое привело к поступлению события Error, извлекается через свойство HttpContext.Error (которое доступно посредством удобного свойства Page.Context). Если это не исключение FormatException, мы ничего не предпринимаем, т.е. будет использоваться стандартная процедура обработки ошибок.

Одно из главных преимуществ обработки события Error настолько близко к точке его возникновения связано с тем, что мы имеем доступ к исключению, которое описывает ошибку, и можем передать информацию через строку запроса веб-форме, генерирующей страницу ошибки. Мы воспользовались этим для добавления типа исключения к отображаемой странице ошибки, как показано на рисунке ниже:

Отображение деталей исключения на странице ошибки

Обработка ошибки на уровне приложения

Необработанные исключения приводят к инициированию события Error, определенного в классе HttpApplication, и в глобальном классе приложения можно предусмотреть специальную обработку для этой ошибки.

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

Различают исключения по тайм-ауту запросов, исключения из-за выхода за пределы массива, а также исключения по причине нулевых ссылок, и разные запросы будут генерировать разные исключения в зависимости от того, на каком этапе обработки база данных стала недоступной. Часто можно снабдить пользователя более полезной информацией, если обратиться к состоянию приложения.

Сброс очередности

Перед демонстрацией этого приема понадобится выполнить определенную работу. Обработчик события Error в глобальном классе приложения будет запущен только в случае, если исключение нигде не было обработано. Это означает, что событие Error, определенное в классе Page, и атрибут ErrorPage, определенный в директиве Page, имеют преимущество (сначала событие, затем атрибут директивы, если исключение так и не было обработано).

В методе обработки события Error, который определен в файле отделенного кода Default.aspx.cs, мы обрабатываем только исключение FormatException. Это значит, что исключение ArgumentNullException, генерируемое в результате отметки флажка, поднимется на следующий уровень, которым является атрибут ErrorPage. Нам нужно, чтобы исключение добралось до глобального класса приложения, поэтому мы удаляем атрибут ErrorPage из директивы Page:

<%@ Page Language="C#" AutoEventWireup="true"  
    CodeBehind="Default.aspx.cs" Inherits="ErrorHandling.Default" %>
...

Реализация обработчика ошибок на уровне приложения

Чтобы продемонстрировать данный подход к обработке ошибок на примере, мы добавили в проект глобальный класс приложения Global.asax и определили в нем декларативный обработчик для события Error, как показано в примере ниже:

using System;

namespace ErrorHandling
{
    public enum Failure
    {
        None,
        Database,
        FileSystem,
        Network
    }

    public class Global : System.Web.HttpApplication
    {
        protected void Application_Error(object sender, EventArgs e)
        {
            Failure failReason = CheckForRootCause();
            if (failReason != Failure.None)
            {
                Response.Redirect(string.Format
                    ("/ComponentError.aspx?errorSource={0}&errorType={1}",
                    failReason.ToString().ToLower(),
                    Context.Error.GetType()));
            }
        }

        private Failure CheckForRootCause()
        {
            // Получить результаты последних проверок
            Array values = Enum.GetValues(typeof(Failure));
            return (Failure)values.GetValue(new Random().Next(values.Length));
        }
    }
}

Мы определили метод по имени CheckForRootCause(), в котором проверяем наличие проблем в основной инфраструктуре. В рассматриваемом примере приложения какая-либо инфраструктура отсутствует, поэтому мы просто выбираем случайное значение из перечисления Failure и применяем его для эмуляции желаемого поведения.

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

При получении события Error мы вызываем метод CheckForRootCause(). Если обнаруживается серьезная проблема (представленная значением перечисления Failure, отличным от None), мы выводим пользователю более осмысленное сообщение, чем удалось бы в противном случае. Это позволяет избежать весьма распространенной ситуации, когда пользователи считают себя ответственными за возникновение проблемы (из-за ввода неправильного пароля, запроса неизвестного URL и т.п.), хотя на самом деле проблема касается самой инфраструктуры.

Когда мы применяем описанный подход в своих проектах, то полагаемся на данные о работоспособности, собираемые с определенной периодичностью. Эти данные формируются на основе прогона простых тестов для базы данных, файловой системы и других основных компонентов инфраструктуры приложения. Мы не рекомендуем активно проверять состояние инфраструктуры при каждом получении события Error, т.к. эти проверки могут отнимать немало времени и выполнять их слишком часто нецелесообразно.

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

Для тестирования построенного примера понадобится запустить приложение, отметить флажок и отправить форму серверу, поскольку только флажок генерирует исключение, которое достигает глобального класса приложения. Возможно, придется сгенерировать несколько ошибок, чтобы увидеть сообщение об отказе, т.к. эмулируемый отказ выбирается случайным образом. Если выбрано значение None, активизируется стандартная обработка ошибок. Пример сообщения приведен на рисунке ниже:

Обработка ошибки в Global.asax

Обработка ошибок без перенаправления

Простейший способ реализации специального подхода к обработке ошибок заключается в перенаправлении браузера на другой URL; мы применяли этот способ в большинстве примеров. Если перенаправление нежелательно, необходимо избрать несколько другой подход. В примере ниже показаны изменения, внесенные в файл Global.asax.cs с целью обработки ошибок без перенаправления:

// ...
protected void Application_Error(object sender, EventArgs e)
{
    Failure failReason = CheckForRootCause();
    if (failReason != Failure.None)
    {
        Response.ClearHeaders();
        Response.ClearContent();
        Response.StatusCode = 200;

        Server.Execute(string.Format
            ("/ComponentError.aspx?errorSource={0}&errorType={1}",
            "the " + failReason.ToString().ToLower(),
            Context.Error.GetType()));

        Context.ClearError();
    }
}
// ...

Метод HttpServerUtility.Execute() применялся для генерации ответа из веб-формы ComponentError.aspx. Метод Execute() создает ответ не выполняя перенаправление запроса (этот метод может использоваться только с веб-формами).

Генерация страницы ошибок без перенаправления порождает две проблемы, которые придется решить. Первая проблема в том, что в момент генерации исключения состояние ответа не было известно, а это значит, что сообщение об ошибке может быть просто добавлено в частично построенный ответ. Это не играет роли, если выполняется перенаправление, поскольку об очистке заботится метод HttpResponse.Redirect(), Но в данном случае все должно делаться явно:

Response.ClearHeaders();
Response.ClearContent();
Response.StatusCode = 200;

Посредством объекта HttpResponse удаляется любой контент и заголовки, которые уже были установлены, чтобы кодом состояния запроса стал 200, указывающий на успешный запрос. (Как объяснялось ранее, специальные ошибки взаимодействуют с пользователем, а не клиентом, поэтому отказавший запрос будет по-прежнему генерировать код состояния 200.)

Вторая проблема связана с необходимостью сообщить среде ASP.NET Framework о том, что мы позаботились об ошибке; это делается вызовом метода HttpContext.ClearError().

Context.ClearError();

Если не вызвать этот метод, ошибка останется ассоциированной с запросом и в дело вступит поддержка обработки ошибок ASP.NET Framework. В рассматриваемом примере это означает применение специальной страницы ошибки, заданной в файле Web.config, которая переопределяет ответ, подготовленный методом обработки события Error.

Пройди тесты
Лучший чат для C# программистов