Пользовательская обработка ошибок ASP.NET MVC Application_Error Global.asax?

108

У меня есть базовый код для определения ошибок в моем приложении MVC. В настоящее время в моем проекте у меня есть контроллер , называемый Errorс методами действий HTTPError404(), HTTPError500()и General(). Все они принимают строковый параметр error. Использование или изменение кода ниже. Каков наилучший / правильный способ передать данные контроллеру ошибок для обработки? Я хотел бы иметь как можно более надежное решение.

protected void Application_Error(object sender, EventArgs e)
{
    Exception exception = Server.GetLastError();
    Response.Clear();

    HttpException httpException = exception as HttpException;
    if (httpException != null)
    {
        RouteData routeData = new RouteData();
        routeData.Values.Add("controller", "Error");
        switch (httpException.GetHttpCode())
        {
            case 404:
                // page not found
                routeData.Values.Add("action", "HttpError404");
                break;
            case 500:
                // server error
                routeData.Values.Add("action", "HttpError500");
                break;
            default:
                routeData.Values.Add("action", "General");
                break;
        }
        routeData.Values.Add("error", exception);
        // clear error on server
        Server.ClearError();

        // at this point how to properly pass route data to error controller?
    }
}
Aherrick
источник

Ответы:

104

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

protected void Application_Error(object sender, EventArgs e) {
  Exception exception = Server.GetLastError();
  Response.Clear();

  HttpException httpException = exception as HttpException;

  if (httpException != null) {
    string action;

    switch (httpException.GetHttpCode()) {
      case 404:
        // page not found
        action = "HttpError404";
        break;
      case 500:
        // server error
        action = "HttpError500";
        break;
      default:
        action = "General";
        break;
      }

      // clear error on server
      Server.ClearError();

      Response.Redirect(String.Format("~/Error/{0}/?message={1}", action, exception.Message));
    }

Тогда ваш контроллер получит все, что вы хотите:

// GET: /Error/HttpError404
public ActionResult HttpError404(string message) {
   return View("SomeView", message);
}

Ваш подход требует компромиссов. Будьте очень осторожны с зацикливанием при такой обработке ошибок. Другое дело, что, поскольку вы проходите конвейер asp.net для обработки 404, вы создадите объект сеанса для всех этих обращений. Это может быть проблемой (производительность) для часто используемых систем.

Андрекарлуччи
источник
Что именно вы имеете в виду, когда говорите «не зацикливайтесь»? Есть ли лучший способ справиться с этим типом перенаправления ошибок (при условии, что это была широко используемая система)?
Aherrick
4
Под циклом я подразумеваю, что когда у вас есть ошибка на странице с ошибкой, вы будете снова и снова перенаправлены на страницу с ошибкой ... (например, вы хотите зарегистрировать свою ошибку в базе данных, а она не работает).
andrecarlucci
125
Перенаправление при ошибках противоречит архитектуре сети. URI должен оставаться неизменным, когда сервер отвечает с правильным кодом состояния HTTP, чтобы клиент знал точный контекст сбоя. Реализация HandleErrorAttribute.OnException или Controller.OnException - лучшее решение. И если это не удается, выполните Server.Transfer ("~ / Error") в Global.asax.
Асбьорн Ульсберг
1
@Chris, это приемлемо, но не лучшая практика. Тем более, что он часто перенаправляется в файл ресурсов, который обслуживается кодом состояния HTTP 200, что оставляет клиенту уверенность, что все прошло хорошо.
Asbjørn Ulsberg 08
1
Мне пришлось добавить <httpErrors errorMode = "Подробный" /> в web.config, чтобы это работало на сервере.
Jeroen K
28

Чтобы ответить на первоначальный вопрос «как правильно передать маршрутизируемые данные контроллеру ошибок?»:

IController errorController = new ErrorController();
errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));

Затем в своем классе ErrorController реализуйте такую ​​функцию:

[AcceptVerbs(HttpVerbs.Get)]
public ViewResult Error(Exception exception)
{
    return View("Error", exception);
}

Это помещает исключение в представление. Страница просмотра должна быть объявлена ​​следующим образом:

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<System.Exception>" %>

И код для отображения ошибки:

<% if(Model != null) { %>  <p><b>Detailed error:</b><br />  <span class="error"><%= Helpers.General.GetErrorMessage((Exception)Model, false) %></span></p> <% } %>

Вот функция, которая собирает все сообщения об исключениях из дерева исключений:

    public static string GetErrorMessage(Exception ex, bool includeStackTrace)
    {
        StringBuilder msg = new StringBuilder();
        BuildErrorMessage(ex, ref msg);
        if (includeStackTrace)
        {
            msg.Append("\n");
            msg.Append(ex.StackTrace);
        }
        return msg.ToString();
    }

    private static void BuildErrorMessage(Exception ex, ref StringBuilder msg)
    {
        if (ex != null)
        {
            msg.Append(ex.Message);
            msg.Append("\n");
            if (ex.InnerException != null)
            {
                BuildErrorMessage(ex.InnerException, ref msg);
            }
        }
    }
Тим Купер
источник
9

Я нашел решение проблемы с ajax, отмеченное Lion_cl.

global.asax:

protected void Application_Error()
    {           
        if (HttpContext.Current.Request.IsAjaxRequest())
        {
            HttpContext ctx = HttpContext.Current;
            ctx.Response.Clear();
            RequestContext rc = ((MvcHandler)ctx.CurrentHandler).RequestContext;
            rc.RouteData.Values["action"] = "AjaxGlobalError";

            // TODO: distinguish between 404 and other errors if needed
            rc.RouteData.Values["newActionName"] = "WrongRequest";

            rc.RouteData.Values["controller"] = "ErrorPages";
            IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
            IController controller = factory.CreateController(rc, "ErrorPages");
            controller.Execute(rc);
            ctx.Server.ClearError();
        }
    }

ErrorPagesController

public ActionResult AjaxGlobalError(string newActionName)
    {
        return new AjaxRedirectResult(Url.Action(newActionName), this.ControllerContext);
    }

AjaxRedirectResult

public class AjaxRedirectResult : RedirectResult
{
    public AjaxRedirectResult(string url, ControllerContext controllerContext)
        : base(url)
    {
        ExecuteResult(controllerContext);
    }

    public override void ExecuteResult(ControllerContext context)
    {
        if (context.RequestContext.HttpContext.Request.IsAjaxRequest())
        {
            JavaScriptResult result = new JavaScriptResult()
            {
                Script = "try{history.pushState(null,null,window.location.href);}catch(err){}window.location.replace('" + UrlHelper.GenerateContentUrl(this.Url, context.HttpContext) + "');"
            };

            result.ExecuteResult(context);
        }
        else
        {
            base.ExecuteResult(context);
        }
    }
}

AjaxRequestExtension

public static class AjaxRequestExtension
{
    public static bool IsAjaxRequest(this HttpRequest request)
    {
        return (request.Headers["X-Requested-With"] != null && request.Headers["X-Requested-With"] == "XMLHttpRequest");
    }
}
Йозеф Крчлявы
источник
При реализации этого я получил следующую ошибку: «System.Web.HttpRequest» не содержит определения для «IsAjaxRequest». В этой статье есть решение: stackoverflow.com/questions/14629304/…
Джулиан Дормон 01
8

Раньше я боролся с идеей централизации процедуры глобальной обработки ошибок в приложении MVC. У меня есть сообщение на форумах ASP.NET .

Он в основном обрабатывает все ошибки вашего приложения в global.asax без необходимости использования контроллера ошибок, украшения с помощью [HandlerError]атрибута или возни с customErrorsузлом в web.config.

Джек Сюй
источник
6

Возможно, лучший способ обработки ошибок в MVC - применить атрибут HandleError к вашему контроллеру или действию и обновить файл Shared / Error.aspx, чтобы делать то, что вы хотите. Объект Model на этой странице включает свойство Exception, а также ControllerName и ActionName.

Брайан
источник
1
Как тогда поступить с 404ошибкой? поскольку для этого нет контроллера / действия?
Dementic
Принятый ответ включает 404. Этот подход полезен только для 500 ошибок.
Брайан
Может, тебе стоит отредактировать это в своем ответе. Perhaps a better way of handling errorsзвучит очень похоже на все ошибки, а не только на 500.
Dementic
4

Application_Error имеет проблему с запросами Ajax. Если ошибка обработана в Action, вызываемом Ajax, он отобразит ваш Error View внутри полученного контейнера.

Виктор Гельмутдинов
источник
4

Это может быть не лучший способ для MVC ( https://stackoverflow.com/a/9461386/5869805 )

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

Global.asax.cs

protected void Application_Error()
{
    var exception = Server.GetLastError();
    // TODO do whatever you want with exception, such as logging, set errorMessage, etc.
    var errorMessage = "SOME FRIENDLY MESSAGE";

    // TODO: UPDATE BELOW FOUR PARAMETERS ACCORDING TO YOUR ERROR HANDLING ACTION
    var errorArea = "AREA";
    var errorController = "CONTROLLER";
    var errorAction = "ACTION";
    var pathToViewFile = $"~/Areas/{errorArea}/Views/{errorController}/{errorAction}.cshtml"; // THIS SHOULD BE THE PATH IN FILESYSTEM RELATIVE TO WHERE YOUR CSPROJ FILE IS!

    var requestControllerName = Convert.ToString(HttpContext.Current.Request.RequestContext?.RouteData?.Values["controller"]);
    var requestActionName = Convert.ToString(HttpContext.Current.Request.RequestContext?.RouteData?.Values["action"]);

    var controller = new BaseController(); // REPLACE THIS WITH YOUR BASE CONTROLLER CLASS
    var routeData = new RouteData { DataTokens = { { "area", errorArea } }, Values = { { "controller", errorController }, {"action", errorAction} } };
    var controllerContext = new ControllerContext(new HttpContextWrapper(HttpContext.Current), routeData, controller);
    controller.ControllerContext = controllerContext;

    var sw = new StringWriter();
    var razorView = new RazorView(controller.ControllerContext, pathToViewFile, "", false, null);
    var model = new ViewDataDictionary(new HandleErrorInfo(exception, requestControllerName, requestActionName));
    var viewContext = new ViewContext(controller.ControllerContext, razorView, model, new TempDataDictionary(), sw);
    viewContext.ViewBag.ErrorMessage = errorMessage;
    //TODO: add to ViewBag what you need
    razorView.Render(viewContext, sw);
    HttpContext.Current.Response.Write(sw);
    Server.ClearError();
    HttpContext.Current.Response.End(); // No more processing needed (ex: by default controller/action routing), flush the response out and raise EndRequest event.
}

Посмотреть

@model HandleErrorInfo
@{
    ViewBag.Title = "Error";
    // TODO: SET YOUR LAYOUT
}
<div class="">
    ViewBag.ErrorMessage
</div>
@if(Model != null && HttpContext.Current.IsDebuggingEnabled)
{
    <div class="" style="background:khaki">
        <p>
            <b>Exception:</b> @Model.Exception.Message <br/>
            <b>Controller:</b> @Model.ControllerName <br/>
            <b>Action:</b> @Model.ActionName <br/>
        </p>
        <div>
            <pre>
                @Model.Exception.StackTrace
            </pre>
        </div>
    </div>
}
буркай
источник
Это лучший способ ИМО. Именно то, что я искал.
Стив Харрис,
@SteveHarris рад, что это помогло! :)
burkay
3

Брайан, этот подход отлично подходит для запросов, отличных от Ajax, но, как заявил Lion_cl, если у вас есть ошибка во время вызова Ajax, ваше представление Share / Error.aspx (или ваше настраиваемое представление страницы с ошибкой) будет возвращено вызывающей стороне Ajax - -пользователь НЕ будет перенаправлен на страницу с ошибкой.

undeniablyrob
источник
0

Используйте следующий код для перенаправления на страницу маршрута. Используйте exception.Message как признак исключения. Строка запроса исключения Coz дает ошибку, если она увеличивает длину строки запроса.

routeData.Values.Add("error", exception.Message);
// clear error on server
Server.ClearError();
Response.RedirectToRoute(routeData.Values);
Свапнил Малап
источник
-1

У меня проблема с этим подходом к обработке ошибок: В случае web.config:

<customErrors mode="On"/>

Обработчик ошибок выполняет поиск в представлении Error.shtml и шаге потока управления в Application_Error global.asax только после исключения

System.InvalidOperationException: представление «Ошибка» или его главное устройство не было найдено, либо механизм представления не поддерживает найденные местоположения. Был произведен поиск в следующих местах: ~ / Views / home / Error.aspx ~ / Views / home / Error.ascx ~ / Views / Shared / Error.aspx ~ / Views / Shared / Error.ascx ~ / Views / home / Error. cshtml ~ / Views / home / Error.vbhtml ~ / Views / Shared / Error.cshtml ~ / Views / Shared / Error.vbhtml в System.Web.Mvc.ViewResult.FindView (контекст ControllerContext) ........ ............

Так

 Exception exception = Server.GetLastError();
  Response.Clear();
  HttpException httpException = exception as HttpException;

httpException всегда имеет значение null, тогда customErrors mode = "On" :( Это вводит в заблуждение Тогда <customErrors mode="Off"/>или <customErrors mode="RemoteOnly"/>пользователи видят customErrors html, Then customErrors mode = "On", этот код тоже неверен


Еще одна проблема этого кода:

Response.Redirect(String.Format("~/Error/{0}/?message={1}", action, exception.Message));

Страница возврата с кодом 302 вместо реального кода ошибки (402,403 и т. Д.)

Александр Шмыков
источник