Понятие ошибок

63

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

Подготовка проекта для примера

В качестве примера мы создаем в Visual Studio новый проект под названием ErrorHandling с использованием шаблона ASP.NET Empty Web Application (Пустое веб-приложение ASP.NET). Затем мы добавляем в проект новый элемент управления по имени SumControl.acsx с помощью шаблона элемента Web User Control (Пользовательский веб-элемент управления). Содержимое файла SumControl.acsx приведено в примере ниже:

<%@ Control Language="C#" AutoEventWireup="true" 
    CodeBehind="SumControl.ascx.cs" Inherits="ErrorHandling.SumControl" %>

<div class="panel">
    <label>1е число:</label>
    <input name="first" value="10"/>
</div>
<div class="panel">
    <label>2е число:</label>
    <input name="second" value="31"/>
</div>
<asp:PlaceHolder ID="resultPlaceholder" runat="server" Visible="false">
    <div class="panel">
        Сумма чисел: <span id="result" runat="server"></span>
    </div>
</asp:PlaceHolder>

Цель этого элемента управления - позволить генерировать ошибки за счет отправки данных формы. Для этого определена пара элементов <input>, в которых пользователь может вводить числовые значения, и элемент <span>, применяемый для отображения суммы введенных значений. Мы используем элемент управления PlaceHolder для сокрытия результата, пока выполняется вычисление. В примере ниже показано содержимое файла отделенного кода, в котором производится реагирование на запросы:

using System;

namespace ErrorHandling {
    public partial class SumControl : System.Web.UI.UserControl {
        protected void Page_Load(object sender, EventArgs e)
        {
    if (IsPostBack)
    {
        int? first = Int32.Parse(Request.Form["first"]);
        int? second = Int32.Parse(Request.Form["second"]);
        result.InnerText = (first + second).ToString();
        resultPlaceholder.Visible = true;
    }
        }
    }
}

Когда поступает запрос POST, мы извлекаем значения формы, преобразуем их в числа и выполняем сложение. В элементе <span> установлен атрибут runat, что позволяет ссылаться на элемент с помощью переменной result (ее имя — это значение атрибута id элемента). С применением свойства InnerText устанавливается контент этого элемента. Свойству Visible элемента управления PlaceHolder присваивается значение true, поэтому результат вычисления будет виден пользователю.

Кроме того, мы добавляем в проект веб-форму по имени Default.aspx, контент которой приведен в примере ниже:

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

<%@ Register TagPrefix="CC" TagName="Sum" Src="~/SumControl.ascx" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title></title>
    <style>
        div.panel {margin-bottom: 5px; clear: both;}
        div.panel label, div.panel input:not([type=checkbox])  {
    display:inline-block;width: 110px;}
        div.wrapper {border: thin solid black; margin-right: 5px; margin-bottom: 
    5px; padding: 5px; float: left; height: 150px; min-width: 100px;}
    </style>
</head>
<body>
    <form id="form1" runat="server">
        <div class="wrapper"><h3>Страница</h3>
    <div class="panel">
        <input type="checkbox" name="pageAction" value="error" />
        Генерировать ошибку
    </div>
        </div>
        <div class="wrapper">
    <h3>Элемент управления</h3>
    <CC:Sum ID="sumControl" runat="server" />
        </div>
        <div class="panel"><button type="submit">Отправить</button></div>
    </form>
</body>
</html>

Веб-форма Default.aspx содержит флажок, который будет использоваться для эмуляции ошибок, и элемент управления SumControl. Класс отделенного кода веб-формы показан в примере ниже:

using System;

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

        protected void Page_Load(object sender, EventArgs e) {
    if (IsPostBack && Request.Form["pageAction"] == "error") {
        throw new ArgumentNullException();
    }
        }
    }
}

После создания веб-формы щелкните правой кнопкой мыши на элементе Default.aspx в окне Solution Explorer среды Visual Studio и выберите в контекстном меню пункт Set As Start Page (Установить в качестве стартовой страницы). В последующих статьях мы планируем создать еще несколько веб-форм, которые будут отображать сообщения об ошибках. Делая Default.aspx стартовой страницей, мы гарантируем, что пользователю не отобразится начальная страница, которая при нормальных обстоятельствах вообще не должна быть видна.

Если флажок "Генерировать ошибку" отмечен, мы генерируем исключение ArgumentNullException. Вывод из веб-формы Default.aspx представлен на рисунке ниже. Хотя HTML-разметка выглядит довольно простой, ее вполне достаточно для отображения деталей в ситуации, когда возникает ошибка.

Базовая форма для примера

Везде в этой и последующих статьях приложение должно запускаться выбором пункта Start Without Debugging (Начать без отладки) в меню Debug (Отладка) среды Visual Studio. Если запустить веб-приложение в отладчике, Visual Studio покажет место в приложении, где произошел отказ.

Обычно это полезно, но тема данной статьи не связана с отладкой и мы хотим сосредоточить внимание на том, что происходит в производственной среде. По этой причине приложение необходимо запускать без отладки. Если же вы все-таки запустили его в отладчике и Visual Studio отображает детали исключения C#, просто нажмите клавишу <F5>, чтобы продолжить выполнение.

Ошибки в приложениях ASP.NET

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

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

Все ошибки в ASP.NET представлены исключениями C#, а отказ - это исключение, которое не было обработано. В мире .NET исключения представлены стандартными классами C#, унаследованными от базового класса Exception (этот подход аналогичен другим популярным системам разработки, например, Java и Android). Безопасная разработка веб-приложений ASP.NET и разработка приложений для android предполагают обработку таких исключений, распространяющихся вверх по инфраструктуре (ASP.NET или Android), которая располагает стандартной политикой для работы с ними.

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

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

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

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

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

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

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

Итак, лучше всего начать с анализа того, как среда ASP.NET имеет дело с ошибками по умолчанию. Чтобы сделать это, запустите приложение, выбрав пункт Start Without Debugging в меню Debug среды Visual Studio (это последний раз, когда мы напоминаем о необходимости запуска приложения без отладки). Измените значение в первом элементе <input> на "яблоко". Щелкните на кнопке "Отправить" и вы увидите стандартную обработку отказа в действии:

YSOD - экран смерти ASP.NET

Если вы работали с ASP.NET ранее, то наверняка сталкивались с таким выводом. Его часто называют "желтым экраном смерти" (yellow screen of death - YSOD) - по аналогии с хорошо известным "синим экраном смерти" в операционной системе Windows. "Желтый экран смерти" очень полезен для разработчиков приложения. Он содержит детальные сведения о необработанном исключении, удобную трассировку стека, а также имя файла, в котором возникло исключение - в данном случае необработанное.

Как видно на рисунке выше, было сгенерировано исключение System.FormatException из-за того, что мы попытались преобразовать значение "яблоко" в тип int. Вот оператор, подсвеченный на экране YSOD:

int first = int.Parse(Request.Form["first"]);

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

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

Настройка стандартного поведения

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

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

Предоставление универсальной страницы ошибок

Первый прием, который мы применяем в каждом создаваемом проекте - это настройка статического HTML-файла, отображающего общее сообщение об ошибке. В качестве демонстрации мы добавили в проект новый файл по имени Failure.html, используя шаблон элемента HTML Page (HTML-страница). Содержимое этого файла приведено в примере ниже:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <style>
        body { font-family: sans-serif;}
    </style>
</head>
<body>
    <h1>Извините</h1>
    <p>В приложении возникла какая-то ошибка и мы не смогли обработать ваш запрос.</p>
    <p>Попробуйте повторить операцию.</p>
</body>
</html>

Как и в других примерах, мы концентрируемся на приеме, а не на дизайне пользовательского интерфейса. Страница Failure.html очень проста и всего лишь сообщает пользователю о том, что что-то пошло не так. Конфигурирование ASP.NET Framework для работы с этой HTML-страницей осуществляется в файле Web.config:

<?xml version="1.0"?>

<configuration>

  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
    <customErrors mode="On" defaultRedirect="/Failure.html"></customErrors>
  </system.web>

</configuration>

Элемент customErrors относится к разделу configuration/system.web файла Web.config и в нем определены атрибуты, перечисленные в таблице ниже:

Атрибуты, определенные в элементе customErrors
Имя Описание
defaultRedirect

Указывает URL, который будет использоваться для отображения сообщений об отказах

mode

Устанавливает режим для специальных ошибок. При значении Off для всех запросов, вызывающих ошибку, применяется "желтый экран смерти". При значении On для всех запросов, вызывающих ошибку, используется специальная страница ошибки. При значении RemoteOnly для запросов, отправляемых с локальной машины, применяется "желтый экран смерти", а для удаленных запросов - специальная страница ошибки. Стандартным значением является RemoteOnly

redirectMode

Указывает, как обрабатывается запрос, который дает в результате ошибку. Значение ResponseRedirect приводит к отправке браузеру HTTP-перенаправления, указывающего на URL, который задан в атрибуте defaultRedirect. Значение ResponseRewrite приводит к выводу контента указанного URL в качестве результата текущего запроса. Стандартным значением является ResponseRedirect

Благодаря этой таблице, можно понять, что добавленная к файлу Web.config конфигурация указывает файл Failure.html в качестве специальной страницы ошибки, которая будет применяться ко всем запросам, включая отправленные из локальной машины. Мы не устанавливали атрибут redirectMode, т.е. браузеру будет отправляться HTTP-перенаправление на файл Failure.html.

Значение RemoteOnly для атрибута mode предназначено для того, чтобы позволить воссоздавать ошибки на производственных платформах, обеспечивая отображение трассировки стека, которая включена в экран YSOD. Мы рекомендуем не пользоваться этим значением. Сервер IIS Express будет принимать только локальные подключения, т.е. значение RemoteOnly во время разработки неприменимо. В производственной среде мы рекомендуем полагаться на фиксацию в журнале деталей запросов, которые вызывают ошибки, а не пытаться воспроизводить проблемы на системах, обслуживающих пользователей (разумеется, значение RemoteOnly совершенно неприменимо в случае развертывания приложения в облачной платформе, наподобие Azure).

Чтобы увидеть результаты конфигурирования специальной страницы ошибки, необходимо запустить приложение, ввести в одном из элементов <input> значение "яблоко" и щелкнуть на кнопке "Отправить":

Отображение простого HTML-файла в случае возникновения отказа

Создание динамической страницы ошибки

Взглянув внимательно на рисунок выше, можно заметить, что URL, по которому перенаправляется браузер, содержит строку запроса:

http://localhost:45906/Failure.html?aspxerrorpath=/Default.aspx

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

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

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title></title>
    <style>
        body { font-family: sans-serif;}
    </style>
</head>
<body>
<h1>Извините</h1>
    <p>В приложении возникла какая-то ошибка и мы не смогли обработать ваш запрос.</p>
    <p><a href="<%: Request["aspxerrorpath"] %>">Попробуйте обновить страницу.</a></p>
</body>
</html>

Эта веб-форма генерирует тот же самый базовый ответ что и содержался в файле Failure.html, но здесь применяется параметр строки запроса для трансформирования части текста в ссылку, которая может использоваться для возвращения в место возникновения ошибки:

Request["aspxerrorpath"]

Чтобы увидеть динамический вывод, необходимо изменить файл Web.config:

...
<customErrors mode="On" defaultRedirect="/DynamicFailure.aspx">
...

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

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

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