Mvc обработка ошибок

Сайтостроение |

создано:
6/3/2012
|
опубликовано:
6/3/2012
|
обновлено:
11/16/2022
|
просмотров:
21435

| всего комментариев:
17

ASP.NET MVC Framework умеет многое, и более того может прекрасно расширяться и дополняться. В этой статье поговорим об обработке ошибок. Будут показаны несколько способов.

Содержание

ASP.NET MVC: История одного проекта «Готовимся к старту» (часть 1)
ASP.NET MVC: История одного проекта «Всё ради данных» (часть 2)
ASP.NET MVC: История одного проекта «Шаблоны и внешний вид» (часть 3)
ASP.NET MVC: История одного проекта «Еще немного классов» (часть 4)
ASP.NET MVC: История одного проекта «UI — всё для пользователя» (часть 5)
ASP.NET MVC: История одного проекта «UI — Добавление экспоната» (часть 6)
ASP.NET MVC: История одного проекта «UI — Редактирование экспоната» (часть 7)
ASP.NET MVC: История одного проекта «Обработка ошибок» (часть 8)
ASP.NET MVC: История одного проекта «Фильтрация» (часть 9)
ASP.NET MVC: История одного проекта «Поиск» (часть 10)
ASP.NET MVC: История одного проекта «Облако тегов» (часть 11)
ASP.NET MVC: История одного проекта «Главная страница» (часть 12)

Палки в колеса

ASP.NET MVC Framework умеет многое. Если вам что-то не нравится, или вы просто хотите реализовать что-либо по-другому, MVC Framework не станет «вставлять палки в колеса», а наоборот предоставит дружелюбный интерфейс. Сегодня поговорим про обработку ошибок. Будут описаны четыре с половиной способа реализации, от простого до продвинутого. Выбирать вам предстоит вам, основываясь на конкретном проекте, или на личных предпочтениях.

Вариант первый «Как два пальца об асфальт»

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

public ActionResult Index() {
  throw new ArgumentException("Специальная ошибка для теста");
  //return View();
}

Запустим сайт и увидим ошибку («некрасивая» форма отображения):

Не очень приятная картина, не правда ли? Но всё меняется когда приходят мы поставим вышеуказанный атрибут (можно над методом, а можно охватить все методы поставив его на контроллером). Атрибут имеет параметр ViewName, который, как вы понимаете принимает название представления для перенаправления при возникновении ошибки. Если вы поставите атрибут над контролером, то перенаправление будет всегда на одну страницу. А если над методом, то у каждого из них может быть своя страница с ошибкой. Я поставил над контролером, а значит по умолчанию перенаправление будет на представление Error:

[HandleError() ]
public class SiteController : Controller {
  /// много букв
}

Теперь запустим… Опаньки! не сработал! Ах, чёрт побери, совсем забыл включить обработку ошибок в файле конфигурации (web.config). Итак, чтобы заработал атрибут, надо включить в секцию System.Web строку CustomErrors:

<system.web>
  <!-- много букв-->

  <customErrors mode="On" />

  <!-- много букв-->
</system.web>

Запускаем еще раз! Ура! Вот что я увидел:

Пожалуй немного поясню. По умолчанию в проекте MVCApplication в папке Views/Shared создается представление Error.cshtml. Это как раз то представление (View), куда делает перенаправление при ошибке атрибут HandleError. Я это представление немного изменил:

@model System.Web.Mvc.HandleErrorInfo
@{
  ViewBag.Title = "Error";
}
<h2>
  Ошибка
</h2>
@Html.ShowAlert(@"В результате выполнения запроса
возникла непредвиденная ситуация. Информация о
случившемся уже направлена администраторам системы.
Проблема будет устранена в кратчайшие сроки.
Приносим вам свои извинения за возможно доставленные неудобства.")
<p>
  С уважением, Администрация</p>

Именно это вы и увидели на предыдущей картинке. Уже не плохо, но дальше — лучше.

Вариант второй «Немного летмотивов»

Поехали дальше… Я убрал атрибут и настройку в файле конфигурации web.config для чистоты эксперимента. Контролеры в MVC — это лейтмотив фреймворка. По умолчанию контролеры, которые вы создаете или будете создавать в своем проекте наследуются от базого абстрактного класса Controller:

public abstract class Controller :
  ControllerBase, IActionFilter,
  IAuthorizationFilter, IDisposable,
  IExceptionFilter, IResultFilter {
///...много букв
}

Нам интересен тот факт, что у этого базового контролера есть виртуальный метод OnException:

//
// Summary:
//     Called when an unhandled exception occurs in the action.
//
// Parameters:
//   filterContext:
//     Information about the current request and action.
protected virtual void OnException(ExceptionContext filterContext);

Из описания понятно, что предназначен отслеживать исключения в контролере. А раз он виртуальный, то его можно переопределить в своём контролере, например, следующим образом:

protected override void OnException(ExceptionContext filterContext) {
  var controllerName = filterContext.RouteData.Values["controller"] as String;
  var actionName = filterContext.RouteData.Values["action"] as String;
  var model = new HandleErrorInfo(
    filterContext.Exception,
    controllerName,
    actionName);
  var result = new ViewResult {
    <strong>ViewName = "error",</strong>
    MasterName = string.Empty,
    ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
    TempData = filterContext.Controller.TempData
  };
  filterContext.Result = result;

  // сконфигурируем отправляемый ответ
  filterContext.ExceptionHandled = true;
  filterContext.HttpContext.Response.Clear();
  filterContext.HttpContext.Response.StatusCode = 500;
  filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}

Результатом поделанных махинаций действий будет представление, обратите внимание на жирную строку… Правильно! Всё то же представление (Error.cshtml) об ошибке (назовем его » красивое»). Уже совсем другой компот, потому что появилась какая-то «управляемость» процессом.

Вариант третий «Место встречи изменить нельзя»

Чтобы не писать в каждом контролере один и тот же код на обработку исключений можно сделать немного интереснее. В файле Global.asax в методе Application_Error написать код один раз, например так:

private void Application_Error(Object sender, EventArgs e) {
  var exception = Server.GetLastError();
  if (exception == null)
    return;

  //  ваша реализация обработки ошибки
  // вплоть до отправки ее на электронную почту
  // администратора системы

  // очищаем ошибку
  Server.ClearError();

  // перенаправляем пользователя на другую страницу
  // созданную специльно для этого.
  <strong>Response.Redirect("site/feedback");</strong>
}

Строка выделенная жирным перенаправит пользователя на «правильную» страницу в зависимости от типа ошибки, это как пример. Вы можете создать много таких специализированных страниц.

Примечание. Если реализуете третий вариант и второй, то приоритет на срабатывание первым будет иметь OnException и только потом уже Application_Error.

Вариант четверный «Любимый»

Я всегда использую этот вариант. Он немного посложнее, но целиком оправдывает свою сложность. Итак, первым делом я создаю новый контролер ErrorController. Он небольшой я приведу его весь:

public class ErrorController : Controller
{
  public ActionResult General(Exception exception) {
    return View("Exception", exception);
  }

  public ActionResult Http404() {
    return View("404");
  }

  public ActionResult Http403() {
    return View("403");
  }

  public ActionResult ExhibitNotFound() {
    return View();
  }
}

Также создаю все указанные представления (View) для методов. ExhibitNotFound.cshtml, 404.cshtml и 403.cshtml содержат просто текст с информацией, что «экспонат не найден»,»страница не найдена» или «доступ ограничен» соответственно, а General ключевое представление, поэтому покажу его полностью:

@model Exception
@{
  ViewBag.Title = "Exception";
  Layout = "~/Views/Shared/_LayoutExtended.cshtml";
}
<h2>
  Ошибка сайта</h2>
<p style="font-size: 1.2em; color: Red; font-weight: bold;">
  Сообщение об ошибке уже отправлено разработчикам. Надеемся на ваше понимание.</p>
<p style="font-weight: bold;">@Model.Message</p>
<p>
  Вы можете:</p>
<ul>
  <li>Перейти на главную @Html.ActionLink("страницу", "index", "site"). </li>
  <li>Сообщить о том, что Вы искали или при каких условиях появилась этот ошибка. Напишите
    разработчикам @Html.ActionLink("сообщение", "feedback", "site")</li></ul>
<fieldset style="font-size: .85em;">
  <legend>Информация для разработчиков</legend>
  @Html.Raw(@Model.StackTrace.ToString())
</fieldset>

После этого я в Global.asax пишу метод Application_Error примерно так:

protected void Application_Error() {
#if !DEBUG
  var exception = Server.GetLastError();
  var httpException = exception as HttpException;
  Response.Clear();
  Server.ClearError();
  var routeData = new RouteData();
  routeData.Values["controller"] = "Error";
  routeData.Values["action"] = "General";
  routeData.Values["exception"] = exception;
  Response.StatusCode = 500;
  if (httpException != null) {
    Response.StatusCode = httpException.GetHttpCode();
    switch (Response.StatusCode) {
    case 403:
      routeData.Values["action"] = "Http403";
      break;
    case 404:
      routeData.Values["action"] = "Http404";
      break;
    }
  }
  Response.TrySkipIisCustomErrors = true;
  IController errorsController = new ErrorController();
  HttpContextWrapper wrapper = new HttpContextWrapper(Context);
  var rc = new RequestContext(wrapper, routeData);
  errorsController.Execute(rc);
#endif

}

Если запустить мой проект сейчас, то я увижу такою картинку:

Мне кажется, это более интересная реализация обработки ошибок. Причем стек можно отображать только при условии, если пользователь имеет права администратора. А самое главное, что теперь можно немного усовершенствовать полученный результат и записывать ошибки в базу данных, создать, своего рода, журнал изменений (Logs), в который можно писать не только ошибки (Errors), но и предупреждения (Warnings), и просто информацию (Information) о действиях пользователя или статусов системы.

Вариант четвертный с половиной или новый класс (Log)

Создаю просто класс, который будет использоваться при работе с логами:

public class Log {
  public Log() { }
  /// <summary>
  /// Создает экземпляр записи в журнале документов
  /// </summary>
  /// <param name="message">текст сообщения</param>
  public Log(string message) {
    this.Message = message;
    this.CreatedAt = DateTime.Now;
  }

  [Key]
  [Display(Name = "Идентификатор")]
  public int Id { get; set; }

  /// <summary>
  /// Текст сообщения
  /// </summary>
  [Required]
  [Display(Name = "Текст сообщения о событии")]
  [StringLength(500)]
  public string Message { get; private set; }

  /// <summary>
  /// Дата выполнения операции
  /// </summary>
  [Required]
  [Display(Name = "Выполнено")]
  public DateTime CreatedAt { get; private set; }

  /// <summary>
  /// Имя пользователя
  /// </summary>
  [Required]
  [ Display(Name = "Автор")]
  [ StringLength(50)]
  public string UserName { get; set; }
}

А теперь при помощи MvcScaffolding создаю репозиторий:

PM> Scaffold Repository Log -DbContextType MuseumContext
Added 'Logs' to database context 'Calabonga.Mvc.Humor.Engine.MuseumContext'
Added repository 'ModelsLogRepository.cs'
PM>

 А теперь придется немного поправить код в репозитории, потому что это у меня не очень обычный репозиторий. Добавим и удалим некоторые методы придеживаясь такого интерфейса:

public interface ILogRepository {
  IQueryable<Log> All { get; }
  Log Find(int id);
  void Log(string message, bool sendNotify);
  void Log(string messageformat, string param0);
  void Log(string messageformat, string param0, bool sendNotify);
  void Log(string messageformat, string param0, string param1);
  void Log(string messageformat, string param0, string param1, bool sendNotify);
  void Delete(int id);
  void Clear();
}

Вы можете придумать свою реализацию этого интерфейса, да и, собственно говоря, и сам интерефейс тоже можете себе придумать сами. А мне остается только добавить запись в журнал в методе Application_Error изпользуя новый LogRepository (жирным шрифтом):

protected void Application_Error() {
#if !DEBUG
  var exception = Server.GetLastError();
  var httpException = exception as HttpException;
  Response.Clear();
  Server.ClearError();
  var routeData = new RouteData();
  routeData.Values["controller"] = "Error";
  routeData.Values["action"] = "General";
  routeData.Values["exception"] = exception;
  Response.StatusCode = 500;
  if (httpException != null) {
    Response.StatusCode = httpException.GetHttpCode();
    switch (Response.StatusCode) {
      case 403:
        routeData.Values["action"] = "Http403";
        break;
      case 404:
        routeData.Values["action"] = "Http404";
        break;
    }
  }
  Response.TrySkipIisCustomErrors = true;
  IController errorsController = new ErrorController();
  <strong>LogRepository logger = new LogRepository();
  logger.Log(string.Concat("ОШИБКА: ", exception.Message));
</strong>  HttpContextWrapper wrapper = new HttpContextWrapper(Context);
  var rc = new RequestContext(wrapper, routeData);
  errorsController.Execute(rc);
#endif
}

У вас наверное возникнет вопрос, почему именно так? Потому что если возникает ошибка или «выстреливает» какое-нибудь исключение, то получить ILogRepository через Dependency Injection уже не получится, поэтому я создаю экземпляр класса и использую его напрямую. Но в контролерах я буду получать именно ILogRepository через Dependency Injection в конструкторе, как и положено.

И напоследок

Я обычно в методах, которые должны каким-то образом реагировать на ошибки и исключения писал TODO, например как этом методе:

public ActionResult Show(int id) {
  Exhibit exh = exhibitRepository.Find(id);
  if (exh != null) {
    return View(new ShowExhibitViewModel(exh));
  }
  // TODO: пока при отсутвии обработки ошибок
  // буду перекидывать на страницу списка
  return RedirectToAction("index");
}

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

public ActionResult Show(int id) {
  Exhibit exh = exhibitRepository.Find(id);
  if (exh != null) {
    return View(new ShowExhibitViewModel(exh));
  }
  return RedirectToAction("http404", "error");
}

Таким образом, пользователь получит правильное уведомление о том что такой записи в базе нет.

Заключение

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

И, как я обещал в комментариях к предыдущей статье, вот [ссылка удалена] на скачивание проекта в текущей сборке. Спасибо за внимание — пишите комментарии.

Сегодня обсудим, как на asp.net mvc можно настроить обработку ошибок 404, 500, ну и любых других. Рассмотрим на примере 404 и 500, как наиболее популярных и важных. Как вместо стандартного не очень красивого желтого окна ошибки показывать свои собственные красивые интересные страницы, и при этом как правильно отдавать код ошибки в браузер пользователя.

Казалось бы, задача довольно тривиальная и может быть решена написанием буквально пары строк кода. Действительно, так и есть, если вы используете любую популярную серверную технологию. Но только не ASP.NET. Если ваше приложение написано на ASP.NET MVC, и вы первый раз сталкиваетесь с проблемой обработки ошибок, очень легко запутаться и сделать неправильные настройки. Что впоследствии негативно отразится на продвижении сайта в поисковых системах, удобстве работы для пользователя, SEO-оптимизации.

Рассмотрим два подхода, как настроить страницы ошибок. Они в целом похожи, какой выбрать – решать вам.

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

Код ответа 200. Это значит что все ОК. Запрос клиента обработан успешно, и сервер отдал затребованные клиентом данные в полном объеме. Например, пользователь кликнул по гиперссылке, и в ответ на это в браузере отобразилась нужная ему информация.

Код ответа 404. Это означает, что запрошенный клиентом ресурс не найден на сервере. Например, указанная в адресе гиперссылки статья не найдена, или *.pdf файл был удален и теперь недоступен для скачивания.

Код ответа 500. Внутренняя ошибка на сайте. Что-то сломалось. Это может быть все что угодно, от неправильно написанного кода программистом, до отказа оборудования на сервере.

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

Стандартная страница ошибки

Стандартная страница ошибки

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

Вариант 1. Ссылка на статичные заранее подготовленные html-страницы.

Первым делом в файле web.config в разделе system.web добавляем новую секцию customErrors со следующими настройками:

web.config

<system.web>
  <customErrors mode="On" redirectMode="ResponseRewrite" defaultRedirect="~/404.aspx">
    <error statusCode="404" redirect="~/404.aspx"/>
    <error statusCode="500" redirect="~/500.aspx"/>
  </customErrors>
  ...
</system.web>

Эта секция служит для обработки ошибок на уровне платформы ASP.NET.

Атрибут mode=»On» определяет, что пользовательские страницы ошибок включены. Также допустимы значения Off / RemoteOnly.

Атрибут redirectMode=»ResponseRewrite» определяет, следует ли изменять URL-адрес запроса при перенаправлении на пользовательскую страницу ошибки. Естественно, нам этого не нужно.

Атрибут defaultRedirect=»~/404.aspx» указывает на то, какая страница ошибки будет показана в случае возникновения кода ответа сервера, который мы не описали в настройках. Пусть при любых других ошибках пользователь будет думать, что страница не найдена.

И уже внутри этой секции мы определяем два кода, для которых у нас будут кастомные страницы ошибок.

Далее, как видно из настроек выше, нам понадобятся *.aspx файлы, на которые будет делаться редирект. Обратите внимание, что мы ссылаемся именно на *.aspx файлы, а не на *.html. Эти файлы являются проходными, служебными, в них содержатся настройки для ответа сервера. Содержимое файла 404.aspx:

404.aspx

<%@ Page Language="C#" %>

<%
    var filePath = MapPath("~/404.html");
    Response.StatusCode = 404;
    Response.ContentType = "text/html; charset=utf-8";
    Response.WriteFile(filePath);
%>

В коде выше мы указываем путь непосредственно до конечного *.html файла, а также дополняем настройки ответа сервера. Указываем код ответа, тип отдаваемого контента и кодировку. Если не указать кодировку, то браузер пользователя может интерпретировать ответ от сервера как не отформатированную строку, и, соответственно, не преобразует ее в html-разметку. А если не указать StatusCode = 404 , то получится следующая интересная ситуация:

Код ответа сервера отдается неверно

Код ответа сервера отдается неверно

И хотя на рисунке выше нам показывается пользовательская страница с ошибкой, при этом код ответа 200 — это конечно же неверно. Когда-то давно на форумах Microsoft такое поведение зарепортили как баг. Однако Microsoft возразила, что это не баг, а фича и не стала ничего менять в будущих релизах ASP.NET. Поэтому приходится это исправлять вручную, и вручную в *.aspx файле в ответе сервера указывать код ответа 404.

Попробуйте собственноручно намеренно убрать какую-нибудь из объявленных на данный момент настроек из секции customErrors и понаблюдайте за результатом.

Также по аналогии создаем подобный *.aspx файл для ошибки 500.

И уже после этого нам нужно создать статичные html-файлы, соответственно для ошибок 404 и 500. Пусть они лежат в корне нашего проекта.

Статичные файлы расположены в корне проекта

Статичные файлы расположены в корне проекта

Здесь же в файле web.config определяем раздел system.WebServer, если он еще не определен, и в нем объявляем секцию httpErrors:

web.config

  <system.webServer>
    <httpErrors errorMode="Custom" defaultResponseMode="File" defaultPath="c:projectsmysite404.html">
      <remove statusCode="404" />
      <remove statusCode="500" />
      <error statusCode="404" path="404.html" responseMode="File" />
      <error statusCode="500" path="500.html" responseMode="File" />
    </httpErrors>
  </system.webServer>

Эта секция служит для обработки ошибок на уровне сервера IIS. Суть в том, что иногда обработка запроса происходит непосредственно на уровне ASP.NET. А иногда ASP.NET просто определяет нужный код ответа и пропускает запрос выше, на уровень сервера. Такой сценарий может случиться, если, например, мы в действии контроллера возвращаем экземпляр класса HttpNotFound:

Или же когда система маршрутизации в MVC-приложении не может определить, к какому маршруту отнести запрошенный пользователем URL-адрес:

https://site.com/long/long/long/long/path

Для секции httpErrors важно отметить следующее. Так как мы ссылаемся на статичные *.html файлы, то и в качестве значений для нужных атрибутов здесь также указываем File . Для атрибута defaultPath необходимо указать абсолютный путь до файла ошибки. Относительный путь именно в этом месте работать не будет. Сам атрибут defaultPath определяет файл, который будет выбран для всех других ошибок, которые мы явно не указали. Но здесь есть одна небольшая проблема. Дело в том, что этот атрибут по умолчанию заблокирован на сервере IIS Express. Если вы разрабатываете свое приложение именно на локальном сервере, то это ограничение нужно снять. Для этого в директории своего проекта нужно найти файл конфигурации сервера и удалить этот атрибут из заблокированных, как это показано на рисунке:

Расположение файла applicationhost.config

Расположение файла applicationhost.config

Также проверьте папку App_Start. Если вы создали не пустое приложение, а работаете над реальным проектом, там может находиться класс FilterConfig, в котором регистрируются все глобальные фильтры в приложении. В методе регистрации удалите строчку кода, где регистрируется HandleErrorAttribute, в нашем случае он не понадобится.

Вот такой комплекс мер нужно предпринять, чтобы настроить обработку ошибок 404, 500, и любых других. Это настройки в файле web.config, и добавление в наш проект статичных файлов.

Вариант 2. Обработка ошибок с использованием специального контроллера.

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

Далее создадим специальный контроллер, который будет принимать все ошибки, которые мы хотим обрабатывать:

public class ErrorController : Controller
{
    public ActionResult NotFound()
    {
        Response.StatusCode = 404;
        return View();
    }

    public ActionResult Internal()
    {
        Response.StatusCode = 500;
        return View();
    }
}

Также создадим соответствующие представления с нужной нам красивой разметкой.

Также в файле web.config нам нужно изменить настройки в секции httpErrors. Если раньше мы ссылались на статичные html-файлы, то теперь мы будем обращаться по указанным URL, которые мы определили в ErrorController’е, чтобы именно там обрабатывать ошибки:

web.config

<httpErrors errorMode="Custom" existingResponse="Replace" defaultResponseMode="ExecuteURL" defaultPath="/Error/NotFound">
  <remove statusCode="404"/>
  <remove statusCode="500"/>
  <error statusCode="404" path="/Error/NotFound" responseMode="ExecuteURL"/>
  <error statusCode="500" path="/Error/Internal" responseMode="ExecuteURL"/>
</httpErrors>

Вариант 3. Фильтр HandleErrorAttribute

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

Итого

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

How can I correctly handle exceptions thrown from controllers in ASP.NET MVC? The HandleError attribute seems to only process exceptions thrown by the MVC infrastructure and not exceptions thrown by my own code.

Using this web.config

<customErrors mode="On">
    <error statusCode="401" redirect="/Errors/Http401" />
</customErrors>

with the following code

namespace MvcApplication1.Controllers
{
    [HandleError]
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            // Force a 401 exception for testing
            throw new HttpException(401, "Unauthorized");
        }
    }
}

doesn’t result in what I was hoping for. Instead I get the generic ASP.NET error page telling me to modify my web.config to see the actual error information. However, if instead of throwing an exception I return an invalid View, I get the /Shared/Views/Error.aspx page:

return View("DoesNotExist");

Throwing exceptions within a controller like I’ve done above seems to bypass all of the HandleError functionality, so what’s the right way to create error pages and how do I play nice with the MVC infrastructure?

Matthew Manela's user avatar

asked May 1, 2009 at 16:58

Adrian Anttila's user avatar

Adrian AnttilaAdrian Anttila

2,0385 gold badges22 silver badges25 bronze badges

Controller.OnException(ExceptionContext context). Override it.

protected override void OnException(ExceptionContext filterContext)
{
    // Bail if we can't do anything; app will crash.
    if (filterContext == null)
        return;
        // since we're handling this, log to elmah

    var ex = filterContext.Exception ?? new Exception("No further information exists.");
    LogException(ex);

    filterContext.ExceptionHandled = true;
    var data = new ErrorPresentation
        {
            ErrorMessage = HttpUtility.HtmlEncode(ex.Message),
            TheException = ex,
            ShowMessage = !(filterContext.Exception == null),
            ShowLink = false
        };
    filterContext.Result = View("ErrorPage", data);
}

splattne's user avatar

splattne

102k52 gold badges202 silver badges249 bronze badges

answered May 1, 2009 at 17:32

7

Thanks to kazimanzurrashaid, here is what I wound up doing in Global.asax.cs:

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

I’ll be able to add more pages to the HttpContoller based on any additional HTTP error codes I need to support.

Matthew Manela's user avatar

answered May 1, 2009 at 19:51

Adrian Anttila's user avatar

Adrian AnttilaAdrian Anttila

2,0385 gold badges22 silver badges25 bronze badges

1

The HandleError attribute seems to only process exceptions thrown by the MVC infrastructure and not exceptions thrown by my own code.

That is just wrong. Indeed, HandleError will only «process» exceptions either thrown in your own code or in code called by your own code. In other words, only exceptions where your action is in the call stack.

The real explanation for the behavior you’re seeing is the specific exception you’re throwing. HandleError behaves differently with an HttpException. From the source code:

        // If this is not an HTTP 500 (for example, if somebody throws an HTTP 404 from an action method),
        // ignore it.
        if (new HttpException(null, exception).GetHttpCode() != 500) {
            return;
        }

answered May 1, 2009 at 17:24

Craig Stuntz's user avatar

Craig StuntzCraig Stuntz

126k12 gold badges252 silver badges273 bronze badges

2

I don’t think you will be able to show specific ErrorPage based upon the HttpCode with the HandleError Attribute and I would prefer to use an HttpModule for this purpose. Assuming that I have folder «ErrorPages» where different page exists for each specific error and the mapping is specifed in the web.config same as the regular web form application. And the following is the code which is used to show the error page:

public class ErrorHandler : BaseHttpModule{

public override void OnError(HttpContextBase context)
{
    Exception e = context.Server.GetLastError().GetBaseException();
    HttpException httpException = e as HttpException;
    int statusCode = (int) HttpStatusCode.InternalServerError;

    // Skip Page Not Found and Service not unavailable from logging
    if (httpException != null)
    {
        statusCode = httpException.GetHttpCode();

        if ((statusCode != (int) HttpStatusCode.NotFound) && (statusCode != (int) HttpStatusCode.ServiceUnavailable))
        {
            Log.Exception(e);
        }
    }

    string redirectUrl = null;

    if (context.IsCustomErrorEnabled)
    {
        CustomErrorsSection section = IoC.Resolve<IConfigurationManager>().GetSection<CustomErrorsSection>("system.web/customErrors");

        if (section != null)
        {
            redirectUrl = section.DefaultRedirect;

            if (httpException != null)
            {
                if (section.Errors.Count > 0)
                {
                    CustomError item = section.Errors[statusCode.ToString(Constants.CurrentCulture)];

                    if (item != null)
                    {
                        redirectUrl = item.Redirect;
                    }
                }
            }
        }
    }

    context.Response.Clear();
    context.Response.StatusCode = statusCode;
    context.Response.TrySkipIisCustomErrors = true;

    context.ClearError();

    if (!string.IsNullOrEmpty(redirectUrl))
    {
        context.Server.Transfer(redirectUrl);
    }
}
}

Nimantha's user avatar

Nimantha

6,5716 gold badges27 silver badges67 bronze badges

answered May 1, 2009 at 18:52

Kazi Manzur Rashid's user avatar

1

One other possibility (not true in your case) that others reading this may be experiencing is that your error page is throwing an error itself, or is not implementing :

 System.Web.Mvc.ViewPage<System.Web.Mvc.HandleErrorInfo>

If this is the case then you will get the default error page (otherwise you’d get an infinite loop because it would keep trying to send itself to your custom error page). This wasn’t immediately obvious to me.

This model is the model sent to the error page. If your error page uses the same master page as the rest of your site and requires any other model information then you will need to either create your own [HandleError] type of attribute or override OnException or something.

answered May 20, 2009 at 2:09

Simon_Weaver's user avatar

Simon_WeaverSimon_Weaver

138k82 gold badges639 silver badges680 bronze badges

1

     protected override void OnException (ExceptionContext filterContext )
    {
        if (filterContext != null && filterContext.Exception != null)
        {
            filterContext.ExceptionHandled = true;
            this.View("Error").ViewData["Exception"] = filterContext.Exception.Message;
            this.View("Error").ExecuteResult(this.ControllerContext);
        }
    }

crypted's user avatar

crypted

10.1k3 gold badges39 silver badges52 bronze badges

answered Oct 10, 2011 at 12:06

bharat's user avatar

1

I chose the Controller.OnException() approach, which to me is the logical choice — since I’ve chosen ASP.NET MVC, I prefer to stay at the framework-level, and avoid messing with the underlying mechanics, if possible.

I ran into the following problem:

If the exception occurs within the view, the partial output from that view will appear on screen, together with the error-message.

I fixed this by clearing the response, before setting filterContext.Result — like this:

        filterContext.HttpContext.Response.Clear(); // gets rid of any garbage
        filterContext.Result = View("ErrorPage", data);

Nimantha's user avatar

Nimantha

6,5716 gold badges27 silver badges67 bronze badges

answered Aug 10, 2010 at 19:29

mindplay.dk's user avatar

mindplay.dkmindplay.dk

7,0463 gold badges44 silver badges53 bronze badges

2

Jeff Atwood’s User Friendly Exception Handling module works great for MVC. You can configure it entirely in your web.config, with no MVC project source code changes at all. However, it needs a small modification to return the original HTTP status rather than a 200 status. See this related forum post.

Basically, in Handler.vb, you can add something like:

' In the header...
Private _exHttpEx As HttpException = Nothing

' At the top of Public Sub HandleException(ByVal ex As Exception)...
HttpContext.Current.Response.StatusCode = 500
If TypeOf ex Is HttpException Then
    _exHttpEx = CType(ex, HttpException)
    HttpContext.Current.Response.StatusCode = _exHttpEx.GetHttpCode()
End If

answered Apr 23, 2012 at 19:35

Martin_W's user avatar

Martin_WMartin_W

1,5421 gold badge18 silver badges24 bronze badges

Core Concepts:

An error within a piece of software can be many things, but in more recent programming languages, they are commonly referred to as exceptions. An exception is often described as «an event during the execution of a program, that disrupts the normal flow» — in other words, something which was not expected and should be taken care of. This is called exception handling and it’s a very important part of a modern piece of software, including a web application.

In the web world however, with a framework like ASP.NET Core, there are actually more than one type of error to deal with. There’s exceptions, which we’ll talk about in this article, and then there’s the errors we inherit from the web/HTTP technologies. For instance, if the user tries to access a page which doesn’t exist, a 404 error is returned to the visitor. A lot of webservers will have a default page for communicating this to the user, and if not, the browser will take care of it. However, the 404 error is a great example of an error which is not really an exception, that you want to handle gracefully within your application.

To the user/visitor, however, it won’t make much of a difference what type of error or exception occurs — the only thing that matters is how you handle and recover from it, and how you present it to the user. So, in this article, we’ll talk about exceptions and then we’ll discuss the HTTP-based errors in the next article.

Exception handling

I won’t go into too much details about exceptions in general, since that’s more relevant in a C# tutorial, but let’s briefly touch on the subject. First of all, if you already know that an exception might occur, e.g. because you’re calling methods which may throw an exception, you should of course handle it locally, with a try..catch block:

public IActionResult Details(int id)  
{  
    try  
    {  
var product = DbHelper.GetProduct(id);  
return View(product);  
    }  
    catch(Exception ex)  
    {  
// Here you may want to log the exception and return an actual error page
return Content("Sorry, an error occurred!");  
    }  
}

However, a lot of exceptions might be difficult to anticipate, so if you don’t want to wrap all your code inside try..catch blocks, you need to handle exceptions at a higher level. Try opening the Startup.cs file found in your ASP.NET Core MVC project and have a look at the Configure() method. How it looks and what it does depends on the template you have used to create your project, but if you used the Empty template and you haven’t changed it, you will likely find a few lines of code like these:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if(env.IsDevelopment())
    {
app.UseDeveloperExceptionPage();
    }
    ....

They’re basically telling your web application to use the special Developer Exception Page, as long as the application is running in development mode (more on that later). You will see the result of this when an exception is thrown, either because you use the throw keyword or because you do something which results in an exception:

public IActionResult Index()
{
    string s = null;
    return Content(s.Trim());
}

This will, obviously, result in a NullReferenceException — just try accessing the action in your browser and notice the result. Depending on the version of ASP.NET Core you’re using, it will look something like this:

As you can see, ASP.NET generates a very detailed error page for you — from this, you should be able to pinpoint the problem (also when it’s less obvious than in our example) and fix it. For security reasons, this amount of details should only be displayed to the developer(s) and NEVER to the user/visitor. Instead, when we’re not in development mode, we should display a more user friendly error message to the user, probably with an apology, and then log the exception e.g. to a database.

The first part, where we display something user-friendly to the user, is easy. It can be done in many ways, but it all starts with the code we talked about before in the Configure() method of Startup.cs. We need to modify it so that it handles situations where the application is not in development mode, like this:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if(env.IsDevelopment())
    {
app.UseDeveloperExceptionPage();
    }
    else
    {
app.UseExceptionHandler("/Error");
    }
    ....

With the help from the UseExceptionHandler() method, we can redirect the user to a specific path when an error occurs. In this case, we send them to «/Error», so we need a method to handle this path. One approach would be to either define an action on the HomeController called Error() or to create an ErrorController and define an Index() method. I have used the latter approach and it looks like this:

public class ErrorController : Controller
{
    public IActionResult Index()
    {
return Content("We're so sorry, but an error just occurred! We'll try to get it fixed ASAP!");
    }
}

In a real world application, this should of course be replaced with a nice looking View, which can even use the same Layout as the rest of the page!

Exception details

We can now handle an unexpected exception more gracefully, but we still don’t know what went wrong. Since we don’t display any details to the user, we need to handle it before we tell the user that something went wrong. To access information about the exception, we can use the IExceptionHandlerPathFeature interface. It can be found in the Microsoft.AspNetCore.Diagnostics namespace, so you will need to import this where you handle the exception, e.g. in the ErrorController:

using Microsoft.AspNetCore.Diagnostics;

Now we can change the Index() action to actually handle the exception, before telling the user about it:

public class ErrorController : Controller  
{  
    public IActionResult Index()  
    {  
var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();  
if((exceptionHandlerPathFeature != null) && (exceptionHandlerPathFeature.Error != null))  
{  
    // In our example, the ExceptionHelper.LogException() method will take care of  
    // logging the exception to the database and perhaps even alerting the webmaster  
    // Make sure that this method doesn't throw any exceptions or you might end  
    // in an endless loop!  
    ExceptionHelper.LogException(exceptionHandlerPathFeature.Error);  
}  
return Content("We're so sorry, but an error just occurred! It has been logged and we'll try to get it fixed ASAP!");  
    }  
}

With all this in place, your application is now more robust and ready for the unanticipated exceptions which will likely occur.

Summary

In this article, we’ve learned how to deal with exceptions, whether they are anticipated or not. This is an important step in creating a robust web application and you should always have a strategy for this before your web application is released to the public.

Another thing you should consider is how to deal with the various HTTP status codes which your application will be returning to the user, e.g. the 404 code when a page is not found — we’ll discuss this in the next article.

This article has been fully translated into the following languages:

  • Spanish

Is your preferred language not on the list? Click here to help us translate this article into your language!


Index

  • 1. Introduction
  • 2. HandleErrorAttribute
  • 3. Limitations of HandleError
  • 4. HandleError vs Application_Error
  • 5. Extending HandleError
  • 6. Returning views from Application_Error
  • 7. ELMAH
  • 8. Summary

Introduction

Exception handling is a serious matter in any application, whether it’s web or desktop. Implementing a proper exception handling is important in any application.
In most cases once we catch the exception we have to log the exception details to database or text file and show a friendly message to the user.

In ASP.NET applications, error handling is done mostly in two ways: at local level using try-catch blocks and at global level using application events.
ASP.NET MVC comes with some built-in support for exception handling through exception filters. The HandleError is the default built-in exception filter.
Unfortunately, the HandleError filter not gives a complete answer to the exception handling problem and that makes us to still rely on the Application_Error event.

In this article, we will learn about the HandleError filter and discuss about the different exception handling
mechanisms that will fit to an MVC application.

HandleError Attribute

Exception filters

The exception filters are attributes that can be applied over an action or a controller or even at a global level. When you apply the filter at the global level then it will
handle the exceptions raised by all the actions of all the controllers. The exception filters not only catches the exceptions
that are raised by the actions but also the ones that are raised by the action filters that are applied over the actions.

All the exception filters implements the IExceptionFilter interface. Listing 1. shows the definition of this interface.
The IExceptionFilter contains a single method called OnException which will be called whenever an exception occurs. The ExceptionContext parameter which derives from ControllerContext provides access to the controller,
route data and HttpContext.

public interface IExceptionFilter
{
	void OnException(ExceptionContext filterContext);
}

Listing 1. IExceptionFilter definition

The HandleErrorAttribute is the default implementation of the IExceptionFilter. When you create a MVC application you will see the HandleErrorAttribute is added to the
GlobalFiltersCollection in the Global.asax.cs.

public static void RegisterGlobalFilters(GlobalFiltersCollection filters)
{
	filters.Add(new HandleErrorAttribute());
}

Listing 2. Registering HandleErrorAttribute to GlobalFiltersCollection

What the HandleError filter does?

The HandleError filter handles the exceptions that are raised by the controller actions, filters and views, it returns a custom view named Error which is placed
in the Shared folder. The HandleError filter works only if the <customErrors> section is turned on in web.config.

The HandleError filter handle exceptions only if the <customErrors> is turned on in web.config

Error View

The Error view that is created by default contains the following html.

@{
   Layout = null;
}

<!DOCTYPE html>
<html>
<head>
   <meta name="viewport" content="width=device-width" />
   <title>Error</title>
</head>
<body>
   <h2>
       Sorry, an error occurred while processing your request.
   </h2>
</body>
</html>

Listing 3. Error View

It contains nothing other than a simple text message. The Layout property is set to null so that the Error view doesn’t inherits the application’s style.

Accessing the exception details in Error view

In some cases, we have to access the exception information from the Error view. The HandleError filter not only just
returns the Error view but it also creates and passes the HandleErrorInfo model to the view. The HandleErrorInfo model contains
the details about the exception and the names of the controller and action that caused the exception.

Here is the definition of the HandleErrorInfo.

public class HandleErrorInfo
{   
	public HandleErrorInfo(Exception exception, string controllerName, 
		string actionName);

	public string ActionName { get; }

	public string ControllerName { get; }

	public Exception Exception { get; }
}

Listing 4. HandleErrorInfo error model

Though it’s not necessary let’s strongly type the Error view to the HandleErrorInfo model.

@model System.Web.Mvc.HandleErrorInfo

Listing 5. Strongly typing Error view to HandleErrorInfo model

We can easily show the exception and other information by accessing the model through the Model property.

<h2>Exception details</h2>
<p>
	Controller: @Model.ControllerName <br>
	Action: @Model.ActionName
	Exception: @Model.Exception
</p>

Listing 6. Displaying exception details in Error view

Returning different views for different exceptions

We can return different views from the HandleError filter. For ex. if you want to return one view for database exceptions and another view for application exceptions,
you could easily do that by specifying the View and Exception properties as shown in the below listing.

[HandleError(Exception = typeof(DbException), View = "DatabaseError")]
[HandleError(Exception = typeof(AppException), View = "ApplicationError")]
public class ProductController
{
	
}

Listing 7. Setting views for exceptions

The HandleErrorAttribute can be applied multiple times over a controller or action or at global level.

Limitations of HandleError

The HandleError filter has some limitations by the way.

1. Not support to log the exceptions
2. Doesn’t catch HTTP exceptions other than 500
3. Doesn’t catch exceptions that are raised outside controllers
4. Returns error view even for exceptions raised in AJAX calls

Let see one by one.

1. Not support to log the exceptions

Logging is very important in error handling and even simple applications get much benefit by logging the errors to a text file or database.
The HandleError filter suppresses the exceptions once they are handled and all it does is showing a custom view to the user.

2. Doesn’t catch HTTP exceptions other than 500.

The HandleError filter captures only the HTTP exceptions having status code 500 and by-passes the others.
Let’s assume we have an action that returns all the posts published for a particular category.
If the category not exists then we are throwing a 404 HTTP exception.

public ViewResult Posts(string category)
{
	if(_blogRepository.Category(category) == null)
		throw new HttpException(404, "Category not found");

	return _blogRepository.Posts(category);
}

Listing 8. Throwing HttpException from action

If any user passes an invalid category a 404 exception will be thrown from the action and the HandleError don’t catch this error.
Usually in this case, programmers like to show a custom view with a message «The requested category is not found or invalid».
Not only the 404 exceptions, the HandleError doesn’t catch any HTTP exception other than 500.

Handling the HTTP exceptions in filters is right or wrong is a thing for debate. In some cases we may have to bypass the HTTP exceptions to the framework
to take proper action against it. So handling HTTP exceptions in filters depends upon the application and it’s not mandatory.

3. Doesn’t catch exceptions that are raised outside controllers

The HandleError is an exception filter and exception filters are called
only if any exception happens inside the action or in the action filters that are applied over the action.
So if the exception happens some other place the filter will be silent.

For ex. say we have set up a route constraint for a specific route as shown in the below listing.

routes.MapRoute(
	 "Default", 
	 "Strange~Action",
	 new { controller = "NonIE", action = "Action"  },
	 new { browserConstraint = new UserAgentConstraint("IE") } 
);

Listing 9. Adding constraints to routes

Route Constraints restricts a route against matching URLs.

The UserAgentConstraint that we set up in the above route restricts the route to handle requests from only Internet Explorer browsers.
In the implementation of UserAgentConstraint I’m purposely throwing an exception.

public class UserAgentConstraint : IRouteConstraint
{
	private readonly string _restrictAgent;

	public UserAgentConstraint(string restrictAgent)
	{
		_restrictAgent = restrictAgent;
	}

	public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
	{
	  // TODO: return true if the browser is not restricted one.
		throw new NotImplementedException("You forget to implement me");
	}
}

Listing 9. UserAgentConstraint

This exception is thrown at very early stage and HandleError filter won’t be there to catch this. When we access the route
where the above constraint is applied we will see the yellow screen instead of the custom error page.

4. Return error view even in exceptions occurred in AJAX calls.

In case of AJAX calls, if some exception happens the HandleError filter returns the custom error view, which is not useful in the client-side.
It would be great to return a piece of JSON in AJAX exceptions and for that we have to extend the HandleError filter or have to create a custom exception filter.

HandleError vs. Application_Error

Exception filters are not global error handlers and this is an important reason that forces us to still rely on Application_Error event.
Some programmers don’t even use the HandleError filter in their application at all and use only the Application_Error event for doing all the
error handling and logging work. The important problem we face in the Application_Error event is, once the program execution reaches
this point then we are out of MVC context and because of that we can miss some context information related to the exception.

Another important feature that exception filters brings to us is we can handle the exceptions in different ways at different scopes,
this is important in some cases, for ex. when exceptions are raised from one controller we have to return a custom error view and
for other controllers we have to return a different error view, this could be easily accomplished through exception filters
but not easily through the Application_Error event.

The bottom-line is, we need to use the Application_Error event at most of the cases in applications unless we are using a framework like
ELMAH which magically handles all the exceptions. But whether we need to use the HandleError filter or not is totally depend upon the application.
When we need a controller or action level exception handling then we can use the HandleError filter along with the Application_Error event else we can simply ignore the HandleError filter.

Extending HandleError

Most of the cases we have to extend the built-in HandleError filter or have to create a custom exception filter to do some useful job like
logging. Here is an example that shows how to extend the built-in filter to log the exceptions using log4net and return a JSON object for AJAX calls.

public class CustomHandleErrorAttribute : HandleErrorAttribute
{
	private readonly ILog _logger;

	public CustomHandleErrorAttribute()
	{
		_logger = LogManager.GetLogger("MyLogger");
	}

	public override void OnException(ExceptionContext filterContext)
	{
		if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)
		{
			return;
		}

		if (new HttpException(null, filterContext.Exception).GetHttpCode() != 500)
		{
			return;
		}

		if (!ExceptionType.IsInstanceOfType(filterContext.Exception))
		{
			return;
		}

		// if the request is AJAX return JSON else view.
		if (filterContext.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
		{
			filterContext.Result = new JsonResult 
			{ 
				JsonRequestBehavior = JsonRequestBehavior.AllowGet, 
				Data = new 
				{ 
					error = true,
					message = filterContext.Exception.Message
				} 
			};
		}
		else
		{
			var controllerName = (string)filterContext.RouteData.Values["controller"];
			var actionName = (string)filterContext.RouteData.Values["action"];
			var model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
			
			filterContext.Result = new ViewResult
			{
				ViewName = View,
				MasterName = Master,
				ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
				TempData = filterContext.Controller.TempData
			};
		}

		// log the error using log4net.
		_logger.Error(filterContext.Exception.Message, filterContext.Exception);

		filterContext.ExceptionHandled = true;
		filterContext.HttpContext.Response.Clear();
		filterContext.HttpContext.Response.StatusCode = 500;

		filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;      
	}
}

Listing 10. Extending HandleError

Most of the code are same as in the built-in filter. Notice we have ignored the HTTP exceptions so anyway we need to
wire-up the Application_Error event to catch and log the missed exceptions. If the request is an AJAX call then we are returning a JSON object
that contains a boolean and the exception message else we are returning the error view. We are setting the response status code as 500 and
the HandleError filter also does the same, this is important in terms of REST and HTTP standards.

Returning views from Application_Error

In some applications we have to depend upon the Application_Error event for handling all the exceptions or the ones that are missed
by the exception filters. Mostly programmers like to return an MVC view instead of a static page. Though we are out of the MVC context still we
can return a view using a controller (thanks to StackOverflow).

Let’s create an Error controller that return different views for different errors as shown in the below listing.

public class ErrorController : Controller
{
	public ActionResult Index()
	{
		return View();
	}

	public ActionResult NotFound()
	{
		return View();
	}
}

Listing 11. Error Controller

We have to invoke this Error controller from the Application_Error to return a view after the exception is logged.

protected void Application_Error(object sender, EventArgs e)
{
	var httpContext = ((MvcApplication)sender).Context;
	var currentController = " ";
	var currentAction = " ";
	var currentRouteData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(httpContext));

	if (currentRouteData != null)
	{
		if (currentRouteData.Values["controller"] != null && !String.IsNullOrEmpty(currentRouteData.Values["controller"].ToString()))
		{
			currentController = currentRouteData.Values["controller"].ToString();
		}

		if (currentRouteData.Values["action"] != null && !String.IsNullOrEmpty(currentRouteData.Values["action"].ToString()))
		{
			currentAction = currentRouteData.Values["action"].ToString();
		}
	}

	var ex = Server.GetLastError();
	var controller = new ErrorController();
	var routeData = new RouteData();
	var action = "Index";

	if (ex is HttpException)
	{
		var httpEx = ex as HttpException;

		switch (httpEx.GetHttpCode())
		{
			case 404:
				action = "NotFound";
				break;

			// others if any
		}
	}

	httpContext.ClearError();
	httpContext.Response.Clear();
	httpContext.Response.StatusCode = ex is HttpException ? ((HttpException)ex).GetHttpCode() : 500;
	httpContext.Response.TrySkipIisCustomErrors = true;
	
	routeData.Values["controller"] = "Error";
	routeData.Values["action"] = action;

	controller.ViewData.Model = new HandleErrorInfo(ex, currentController, currentAction);
	((IController)controller).Execute(new RequestContext(new HttpContextWrapper(httpContext), routeData));
}

Listing 12. Returning views from Application_Error

We are doing many things in the above code, mainly we are instantiating the Error controller and invoking it by calling the Execute()
method passing the HandleErrorInfo model and this is the model used by the HandleError filter as well.
To know the controller and action that handled the request we have to access the GetRouteData method of RouteTable.Routes passing the httpcontext.

We are using a switch statement just to demonstrate how we can return different error views for different HTTP exceptions.
If you want to take care of AJAX calls you have to change the implementation little as we did in the custom HandleError filter but to keep things simple I’ve ignored that part.

ELMAH

ELMAH (Error Logging Modules and Handlers) is an application-wide error logging facility that is completely pluggable.
You can easily configure ELMAH for an ASP.NET MVC application without much code. The other benefits brought up by ELMAH is, it provides
a custom page where the admin can view the errors including the original yellow screen, also it provides options to send emails, generate RSS etc.

ELMAH logs only unhandled exceptions so we have to signal ELMAH to log the exceptions that are handled. When we use the HandleError filter
and ELMAH in an application we will confused seeing no exceptions are logged by ELMAH, it’s because once the exceptions are handled by the HandleError filter
it sets the ExceptionHandled property of the ExceptionContext object to true and that hides the errors from logged by ELMAH.
A better way to overcome this problem is extend the HandleError filter and signal to ELMAH as shown in the below listing.

public class ElmahHandleErrorAttribute : HandleErrorAttribute
{
	public override void OnException(ExceptionContext filterContext)
	{
		var exceptionHandled = filterContext.ExceptionHandled;
		
		base.OnException(filterContext);
		
		// signal ELMAH to log the exception
		if (!exceptionHandled && filterContext.ExceptionHandled)
			ErrorSignal.FromCurrentContext().Raise(filterContext.Exception);
	}
}

Listing 13. Signaling to Elmah

Summary

In this article we saw about the built-in HandleError filter available with ASP.NET MVC to handle the exceptions.
The HandleError filter has some limitations and most importantly it doesn’t handle all the exceptions that are raised by the application.

Although HandleError filter bring some benefits in customizing error handling at a controller or action level still we have to rely on the Application_Error event.
ELMAH is an error handling module that is easily pluggable to an ASP.NET application. Unlike HandleError, ELMAH catches all the exceptions raised by the application.

Download

Понравилась статья? Поделить с друзьями:
  • Muz ge71va коды ошибок
  • Mutt ошибка sasl аутентификации
  • Mustache armies ошибка при запуске
  • Must declare the scalar variable sql ошибка
  • Mushroom depthmap ошибка