CustomErrors vs 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:
- True: существующий ошибочный ответ пройдет через этот модуль без обработки, что делает полный смысл, учитывая его смысловое значение.
- 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.
Или я пропустил какую-то секретную магию, чтобы решить эту проблему?
Ответы
Ответ 1
Как и вы, я думаю, что 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&controller=Errors&action=NotFound" />
</customErrors>
</system.web>
</configuration>
Поведение правильное для обычного запроса браузера: страница с ошибкой отображается при возникновении ошибки и возвращается код ошибки.
Он также исправляет запрос AJAX/Web API: страница с ошибкой НЕ возвращается. Мы получаем разборную ошибку в JSON или XML.
Вы можете добавить раздел httpErrors
, если вы хотите, чтобы ошибки nonASP.NET были перенаправлены на ваши пользовательские ошибки.
Ответ 2
Я несколько дней пытался решить эту проблему, и я думаю, что единственным действительным решением является сообщение здесь (ответ Starain chen к тому же вопросу, отправленному @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
)
Почему я думаю, что это единственное допустимое решение?
- Он обрабатывает как ASP.NET MVC, так и ошибки уровня IIS (Предполагая IIS7 + и интегрированный конвейер)
- Он возвращает действительные коды статуса http. (Не 302 или 200).
- Он возвращает
200 OK
, если я перехожу непосредственно на страницу с ошибкой: я хотел бы получить 200 OK
, если я перейду к /error/404
.
- Я могу настроить содержимое страницы ошибки. (Использование
ViewBag.ErrorMessage
)
- Если клиент делает ошибочный запрос AJAX и ожидает json-данных (в рамках действий контроллеров), он будет обслуживаться с json-данными с соответствующим кодом состояния.