Перехват исключений в ASP.NET

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

Ядро ASP.NET предоставляет стандартный механизм для обработки исключений, которые до этого не были перехвачены ни в одном из методов. Его использование крайне желательно:

  • Для ведения журнала и анализа исключительных ситуаций.
  • Для сокрытия информации об ошибке, если в web.config параметр customErrors равен "Off".
  • С точки зрения пользовательского интерфейса, лучше выводить контекстно-зависимые сообщения с понятным пользователю описанием ошибки и вариантами дальнейших действий.

В ASP.NET для указанной цели служит метод Application_Error(), размещенный в файле Global.asax. Давайте рассмотрим его на примере обработки исключения HttpRequestValidationException.

Реализация обработки в ASP.NET проекте

Создадим новый проект типа "ASP.NET Web Application". Откроем файл Default.aspx и в любом его месте добавим литерал, поле ввода и кнопку. Например, вот так:

<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
    CodeBehind="Default.aspx.cs" Inherits="ExceptionDemo._Default" %>

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <h2>
        Welcome to ASP.NET, <asp:Literal ID="txtValue" runat="server"></asp:Literal>!
    </h2>

    <asp:TextBox ID="tbValue" runat="server"></asp:TextBox>
    <asp:Button ID="btnSubmit" runat="server" Text="Submit" 
        onclick="btnSubmit_Click" />
    
</asp:Content>

Создадим простейший обработчик нажатия кнопки:

namespace ExceptionDemo
{
    using System;

    public partial class _Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
        }

        protected void btnSubmit_Click(object sender, EventArgs e)
        {
            txtValue.Text = tbValue.Text;
        }
    }
}

Запустим созданное веб-приложение. В поле ввода можно ввести любое имя, которое после нажатия кнопки "Submit" появится в заголовке страницы.

Как известно, ядро ASP.NET содержит механизм контроля получаемых запросов. При подозрении в попытке XSS или другой атаки выполнение веб-приложения будет прервано исключением. Например, если ввести текст, содержащий HTML теги, то результатом будет выброс HttpRequestValidationException.

Для его обработки перейдем к Application_Error(), расположенному в файле Global.asax. Сейчас это просто пустая заготовка. Для получения экземпляра Exception с данными об произошедшем событии вызовем метод Server.GetLastError(). Затем, проверив тип исключения, обработаем его:

void Application_Error(object sender, EventArgs e)
{
    Exception ex = Server.GetLastError();

    if (ex is HttpRequestValidationException) {
        Server.Transfer("RequestError.aspx");
    }
}

Обратите внимание на использование метода Transfer(). Он не только осуществляет переадресацию на указанную страницу, но и сохраняет текущий контекст. Поэтому в коде страницы RequestError.aspx метод Server.GetLastError() вернет тот же экземпляр Exception, что позволит продолжить анализ ситуации. Чтобы указать что ошибка обработана, необходимо сбросить её вызовом метода Server.ClearError().

Осталось создать страницу RequestError.aspx с сообщение об ошибке:

<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true"
    CodeBehind="RequestError.aspx.cs" Inherits="ExceptionDemo.RequestError" %>

<asp:Content ID="Content1" ContentPlaceHolderID="HeadContent" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <p>
        I'm sorry, but HTML tags are not allowed on that page.</p>
    <p>
        Please make sure that your entries do not contain any angle brackets like &lt; or
        &gt;.</p>
    <p>
        <a href='javascript:history.go(-1);'>Go back</a></p>
</asp:Content>

Код RequestError.aspx тогда будет следующий:

namespace ExceptionDemo
{
    using System;

    public partial class RequestError : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            Exception ex = Server.GetLastError();
 
            .........

            Server.ClearError()
        }
    }
}

Как можно заметить, теперь пользователю вводится более понятное сообщение об ошибке. Разумеется, в реальном веб-проекте здесь может быть:

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

Использование Application_Error() для обработки исключений в ASP.NET MVC

Каркас ASP.NET MVC позволяет использовать этот же подход при обработке исключений. Однако, есть несколько отличий, которые существенно меняют сценарий:

Во-первых, необходимо вручную добавить метод Application_Error() в файле Global.asax.

Во-вторых, вместо метода Server.Transfer() используется метод Redirect() с указанием необходимого Контроллера и Действия в виде строки URL. Другой вариант – Response.RedirectToRoute() с последующим Server.ClearError(). В любом случае контекст будет утерян, поэтому необходимо:

  • или осуществить всю обработку в методе Application_Error(), используя Контроллер и  Представление только для уведомления пользователя;
  • или сохранить данные в хранилище и передать в Контроллер код, для их получения обратно.

Пример такого решения:

void Application_Error(object sender, System.EventArgs e)
{
    System.Exception ex = Server.GetLastError();

    if (ex is System.Web.HttpRequestValidationException) {
        // Response.Redirect("/UserProfiles/Error");

        int recordId = LogReposistory.Store(ex);

        Response.RedirectToRoute(
            "User",
            new {
                controller = "Errors",
                action = "HttpError",
                id = recordId
            });
        Server.ClearError();
    }
}

При этом Контроллер будет содержать примерно вот такой код:

namespace MVCDemo.Controllers
{
    public class ErrorsController : Controller
    {
        // GET: /Errors/HttpError/{id}
        public ActionResult HttpError (int id)
        {
            .........
            Exception ex = LogReposistory.Load(id); 
            errorProcessor.Process(ex);

            return this.View("_Error");
        }
    }
}

Такой подход крайне неудобен в применении с точки зрения MVC. Поэтому каркас ASP.NET MVC реализует возможность создания фильтров исключений. Их работа соответствует идеологи шаблона, а получаемая информация содержит дополнительные подробности. Это позволяет осуществить более детальный анализ произошедшей ситуации. С выходом 3 версии появилась возможность задавать фильтры исключений не только для Контроллеров, но и глобальные. Данная тема будет подробно рассмотрена в одной из статей серии "ASP.NET MVC 3 в деталях".

Комментарии (8) -

а каким образом используя IExceptionFilter можно например обработать ошибку подключения к БД?

@ alex: Если кратко - реализовать IExceptionFilter и назначить его на обработку всех исключений приложения (RegisterGlobalFilters). Для полного ответа мне придется написать небольшую статью про IExceptionFilter, а это больше формата комментария.

@ Andrey:
я так и сделал, реализовал интерфейс IExceptionFilter, в нём в методе OnException
отлавливаю ексепшены. Все експшены контролеров и вьюх отлавливаются.
а вот напрмиер експешны которые возникают в global.asax к сожалению не перехватываются.

@ alex: Сегодня как раз столкнулся. Сегодня как раз столкнулся. Исключение при подключении к БД было выловлено в ExceptionFilter, повешенный в RegisterGlobalFilters().

Александр 26.04.2012 22:17:35

По-работе столкнулся с обработкой ошибок в WebPage's. (вывод ошибок на единую страницу для всех отчетов)

Реализовывал обработку так - из Application_Error ошибку запихнул в куки, куки в Response и вызвал Context.Response.Redirect на нужную страницу Error.aspx

А вот с MVC возник затык. Ошибку ловлю, согласно примера - могу передать информацию об ошибке в созданный контроллер ErrorController.

А вот, как передать дальше редирект (и можно ли?) на внешнюю страницу - не могу разобраться. Подскажите, если не в тягость.

Благодарю.

Подскажите, можно ли реализовать подобную схему (с переходом на единую страницу Error.aspx) в MVC?

@ Александр: Чтобы ловить Exception Контроллеров достаточно реализовать свой вариант ErrorHandler и повесить его глобально. Но он не поймает ошибки типа 404.

Поэтому я сделал так в Application_Error вызывается метод, который ловит все подряд. После чего порождается экземпляр ErrorController и ему передаётся Модель с данными об ошибке. Вся обработка и вывод страницы теперь лежит на этом Контроллере.

Александр 28.04.2012 14:09:11

это понятно, непонятно как из ErrorController'а сделать редирект на внешнюю страницу

@ Александр: А зачем на внешнюю? Отображайте сразу из контроллера.
Ну а если сильно надо, то для переадресации у контроллера есть семейство методов Redirect*()

Pingbacks and trackbacks (3)+

Добавить комментарий