CustomErrors против HttpErrors - значительный недостаток дизайна?

Как мы можем знать, например, в чем разница между customErrors и httpErrors? CustomErrors - более старый способ определения страниц ошибок в веб-приложении, но у этого подхода есть некоторые проблемы - например, если вы заботитесь о правильных кодах ответов http, так как подход CustomErrors заключается в перенаправлении на страницу ошибки вместо замены текущего ответа, который разрушает большую часть смысловой целостности сообщения с помощью кодов состояния http.

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

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

Простая конфигурация в качестве примера

<httpErrors existingResponse="Auto" errorMode="Custom">
    <remove statusCode="404"/>
    <error statusCode="404" path="/Error/E404" responseMode="ExecuteURL" />
</httpErrors>

Мы устанавливаем errorMode в Custom, потому что мы хотим протестировать саму обработку ошибок, и для существующих Response устанавливаем значение Auto, которое будет представлять ветвь, зависящую от Response.TrySkipIisCustomErrors:

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

В идеале это позволило бы нам самим обрабатывать некоторые ошибки, например, когда продукта в ../product/id не существует, где мы можем вручную вернуть определенную страницу 404 с информацией о недостающих продуктах и ​​все же позволить модулю HttpErrors обрабатывать все отдыхайте как ../products/namebutshouldbeid или просто ../misspelledandunmatchableurl.

Однако, насколько я вижу, это не работает. Причина кроется во внутреннем методе System.Web.HttpResponse.ReportRuntimeError, который будет вызываться при ошибках времени выполнения (например, не найдены контроллеры / действия) и где у нас есть раздел, который выглядит следующим образом:

// always try to disable IIS custom errors when we send an error
if (_wr != null) {
    _wr.TrySkipIisCustomErrors = true;
}

if (!localExecute) {
code = HttpException.GetHttpCodeForException(e);

// Don't raise event for 404.  See VSWhidbey 124147.
if (code != 404) {
    WebBaseEvent.RaiseRuntimeError(e, this);
}

// This cannot use the HttpContext.IsCustomErrorEnabled property, since it must call
// GetSettings() with the canThrow parameter.
customErrorsSetting = CustomErrorsSection.GetSettings(_context, canThrow);
if (customErrorsSetting != null)
    useCustomErrors = customErrorsSetting.CustomErrorsEnabled(Request);
else
    useCustomErrors = true;
}

Во время первой отладки я увидел, что для useCustomErrors было установлено значение false, и я не мог понять, почему, поскольку я знал, что у меня есть рабочая конфигурация HttpError, поскольку она работает, когда я возвращаю HttpNotFoundResult из контроллера.

Тогда я понял, что это не HttpErrors, а более старые CustomErrors. И CustomErrors явно не знает о HttpErrors.

Баг"

Итак, что происходит, так это то, что Response.TrySkipIisCustomErrors установлен в значение true, и поскольку CustomErrors не определены, он возвращает подробный ответ 404. В этот момент мы бы хотели, чтобы HttpErrors включился, но это не так, потому что TrySkipIisCustomErrors теперь установлен в true.

И мы не можем использовать CustomErrors, так как это вернет нас к проблемам с отвратительными перенаправлениями ошибок.

И причина того, что возврат HttpNotFoundResult работает, заключается в том, что он не вызовет ошибку времени выполнения, а только вернет результат 404, который HttpErrors будет перехватывать, как и ожидалось, при условии, что мы не установим Response.TrySkipIisCustomErrors в true.

Как / может это быть обработано / решено?

Я думаю, что System.Web.HttpResponse.ReportRuntimeError нельзя разрешить устанавливать Response.TrySkipIisCustomErrors в значение true по умолчанию, так как у нас есть другой модуль обработки ошибок, зависящий от этого. Таким образом, этот метод должен также знать о любой конфигурации HttpErrors, либо избегая установки значения TrySkipIisCustomErrors в значение true, если у нас есть какая-либо конфигурация CustomErrors, либо что он обрабатывает конфигурацию HttpErrors вместе с конфигурацией CustomErrors.

Или я пропустил какую-то тайную магию, чтобы решить это?

2 ответа

В течение нескольких дней я пытался решить эту проблему, и я думаю, что единственное правильное решение - это опубликованное здесь (ответ Старейна Чена на тот же вопрос, опубликованный @Alex на forums.asp.net):

(Я немного изменил код)

Код

Создать собственный атрибут ошибки дескриптора

public class CustomHandleErrorAttribute : HandleErrorAttribute {
    public override void OnException (ExceptionContext filterContext) {
        if (filterContext.ExceptionHandled) {
            return;
        }

        var httpException = new HttpException(null, filterContext.Exception);
        var httpStatusCode = httpException.GetHttpCode();

        switch ((HttpStatusCode) httpStatusCode) {
            case HttpStatusCode.Forbidden:
            case HttpStatusCode.NotFound:
            case HttpStatusCode.InternalServerError:
                break;

            default:
                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 = String.Format("~/Views/Hata/{0}.cshtml", httpStatusCode),
                ViewData = new ViewDataDictionary(model),
                TempData = filterContext.Controller.TempData
            };
        }

        // TODO: Log the error by using your own method

        filterContext.ExceptionHandled = true;
        filterContext.HttpContext.Response.Clear();
        filterContext.HttpContext.Response.StatusCode = httpStatusCode;
        filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
    }
}

Используйте этот атрибут ошибки дескриптора в App_Start/FilterConfig.cs

public class FilterConfig {
    public static void RegisterGlobalFilters (GlobalFilterCollection filters) {
        filters.Add(new CustomHandleErrorAttribute());
    }
}

Обработать оставшиеся исключения в Global.asax

protected void Application_Error () {
    var exception = Server.GetLastError();
    var httpException = exception as HttpException ?? new HttpException((Int32) HttpStatusCode.InternalServerError, "Internal Server Error", exception);
    var httpStatusCode = httpException.GetHttpCode();

    Response.Clear();

    var routeData = new RouteData();

    routeData.Values.Add("Controller", "Error");
    routeData.Values.Add("fromAppErrorEvent", true);
    routeData.Values.Add("ErrorMessage", httpException.Message);
    routeData.Values.Add("HttpStatusCode", httpStatusCode);

    switch ((HttpStatusCode) httpStatusCode) {
            case HttpStatusCode.Forbidden:
            case HttpStatusCode.NotFound:
            case HttpStatusCode.InternalServerError:
                routeData.Values.Add("action", httpStatusCode.ToString());
                break;

            default:
                routeData.Values.Add("action", "General");
                break;
        }

    Server.ClearError();

    IController controller = new Controllers.ErrorController();

    // TODO: Log the error if you like

    controller.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
}

Создать ErrorController

[AllowAnonymous]
public class ErrorController : Controller {
    protected override void OnActionExecuting (ActionExecutingContext filterContext) {
        base.OnActionExecuting(filterContext);

        var errorMessage = RouteData.Values["ErrorMessage"];
        var httpStatusCode = RouteData.Values["HttpStatusCode"];

        if (errorMessage != null) {
            ViewBag.ErrorMessage = (String) errorMessage;
        }

        if (httpStatusCode != null) {
            ViewBag.HttpStatusCode = Response.StatusCode = (Int32) httpStatusCode;
        }

        Response.TrySkipIisCustomErrors = true;
    }

    [ActionName("403")]
    public ActionResult Error403 () {
        return View();
    }

    [ActionName("404")]
    public ActionResult Error404 () {
        return View();
    }

    [ActionName("500")]
    public ActionResult Error500 () {
        return View();
    }

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

Создать виды

Создать представления для действий в ErrorController, (403.cshtml, 404.cshtml, 500.cshtml а также General.cshtml)

Почему я думаю, что это единственное правильное решение?

  1. Он обрабатывает ошибки ASP.NET MVC и IIS-уровня (при условии, что IIS7+ и интегрированный конвейер)
  2. Возвращает действительные коды статуса http. (Не 302 или 200)
  3. Возвращается 200 OK если я перейду к странице с ошибкой напрямую: я хотел бы получить 200 OK если я перейду к /error/404,
  4. Я могу настроить содержание страницы ошибки. (С помощью ViewBag.ErrorMessage)
  5. Если клиент делает ошибочный запрос AJAX и ожидает данные json (в рамках действий контроллеров), ему / ей будут предоставлены данные json с соответствующим кодом состояния.

Как и вы, я думаю, что ASP.NET не должен устанавливать TrySkipIisCustomErrors или может добавить опцию, чтобы мы могли ее избежать.

В качестве обходного пути я создал страницу ASPX, которая может передавать запрос на контроллер ASP.NET MVC.

ErrorHandler.aspx.cs

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

Код позади

protected void Page_Load(object sender, EventArgs e)
{
  //Get status code
  var queryStatusCode = Request.QueryString.Get("code");
  int statusCode;
  if (!int.TryParse(queryStatusCode, out statusCode))
  {
    var lastError = Server.GetLastError();
    HttpException ex = lastError as HttpException;
    statusCode = ex == null ? 500 : ex.GetHttpCode();
  }
  Response.StatusCode = statusCode;

  // Execute a route
  RouteData routeData = new RouteData();
  string controllerName = Request.QueryString.Get("controller") ?? "Errors";
  routeData.Values.Add("controller", controllerName);
  routeData.Values.Add("action", Request.QueryString.Get("action") ?? "Index");

  var requestContext = new RequestContext(new HttpContextWrapper(Context), routeData);
  IController controller = ControllerBuilder.Current.GetControllerFactory().CreateController(requestContext, controllerName);
  controller.Execute(requestContext);
}

Использование в web.config

<configuration>
    <system.web>
        <customErrors mode="RemoteOnly" redirectMode="ResponseRewrite" defaultRedirect="/Content/ErrorHandler.aspx">
            <error statusCode="404" redirect="/Content/ErrorHandler.aspx?code=404&amp;controller=Errors&amp;action=NotFound" />
        </customErrors>
    </system.web>
</configuration>

Поведение является правильным при обычном запросе браузера: страница ошибки отображается при возникновении ошибки и возвращается код ошибки. Это также верно для запроса AJAX/Web API: страница ошибки НЕ возвращается. Мы получаем синтаксическую ошибку в JSON или XML.

Вы можете добавить httpErrors раздел, если вы хотите, чтобы ошибки не-ASP.NET были перенаправлены на ваши собственные ошибки.

Другие вопросы по тегам