【问题标题】:CustomErrors vs HttpErrors - A significant design flaw?CustomErrors vs HttpErrors - 一个重大的设计缺陷?
【发布时间】:2014-08-19 08:33:59
【问题描述】:

我们可能知道,例如What is the difference between customErrors and httpErrors?CustomErrors 是在 Web 应用程序中定义错误页面的较旧方法,但这种方法存在一些问题 - 例如,如果您关心正确的 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 因为我们想测试错误处理本身,我们将 existingResponse 设置为 Auto em>,会引入一个依赖于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 将按预期拦截,只要我们避免设置 @ 987654329@ 为真。

应该/如何处理/解决这个问题?

我在想 System.Web.HttpResponse.ReportRuntimeError 默认情况下不应该允许将 Response.TrySkipIisCustomErrors 设置为 true,因为我们有另一个依赖于此的错误处理模块。因此,此方法还需要了解任何 HttpErrors 配置,如果我们有任何 CustomErrors 配置,则避免将 TrySkipIisCustomErrors 设置为 true,或者它处理 HttpErrors 配置连同 CustomErrors 配置。

还是我错过了一些秘密魔法来解决这个问题?

【问题讨论】:

  • 好吧,据我调查,您不能简单地将 CustomErrors 与 HttpErrors 结合起来并使其按预期运行。我最终得到的是完全关闭 CustomErrors,而是添加了一个 HttpModule 来检查响应的状态代码并通过直接调用我的错误控制器来替换任何错误响应。我还添加了一个标志,我们可以使用它来定义调试模式,它只会传递现有的详细错误响应,而不用我们的自定义错误替换它。
  • 您是否尝试过使用“将错误发送到浏览器”和错误页面 -> 500 -> 编辑功能设置 -> “详细错误” stackoverflow.com/questions/2640526/…

标签: asp.net asp.net-mvc iis


【解决方案1】:

我已经为解决这个问题苦苦挣扎了好几天,我认为唯一有效的解决方案是here 发布的解决方案(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.cshtml404.cshtml500.cshtmlGeneral.cshtml

为什么我认为这是唯一有效的解决方案?

  1. 它处理 ASP.NET MVC 和 IIS 级错误(假设 IIS7+ 和集成管道)
  2. 它返回有效的 http 状态代码。 (不是 302 或 200)
  3. 如果我直接导​​航到错误页面,它会返回200 OK:如果我导航到/error/404,我想得到200 OK
  4. 我可以调整错误页面的内容。 (使用ViewBag.ErrorMessage
  5. 如果客户端发出错误的 AJAX 请求并期望 json 数据(在控制器的操作中),他/她将收到带有适当状态代码的 json 数据。

【讨论】:

  • 您为 system.web/customErrors 和 system.webServer/httpErrors 使用了哪些设置?试图使这些恰到好处,以便我们的错误响应不会被 IIS 取代,而且关键异常也不会传递详细信息是具有挑战性的。
  • @AaronLS 在应用程序池设置中选择“集成管道”模式,可能需要&lt;customErrors mode="Off" /&gt;,但不需要httpErrors
  • 我逐个复制了这个字符,但是浏览器(Firefox 和 Chrome)开始以纯 HTML 形式返回所有错误响应。事实上,检查响应的来源,响应的整个正文被包裹在&lt;pre&gt; 标签中。
  • 为了澄清我之前的评论,浏览器调试选项卡出于某种原因将mydomain.com/a(或任何其他非路由路径)的响应显示为plain,而不是text/html。我不知道为什么;调试时,Response.ContentType 仍然是正确的“text/html”。一时兴起,我在 Response.Clear() 行之后立即将 Response.AddHeader("Content-Type", "text/html"); 添加到 Global.asax 代码中,从而解决了问题。
  • @killa-byte Response.ContentType 的值与您在浏览器开发工具网络选项卡中看到的值之间的差异可能表明在某处意外删除了响应标头。恕我直言Response.Clear() 不应该这样做,因为还有另一种方法:Response.ClearHeaders()。我不知道,我和你一样迷茫。你能检查Response.Headers前后Response.Clear()的内容吗?
【解决方案2】:

和你一样,我认为 ASP.NET 不应该设置 TrySkipIisCustomErrors 或者可以添加一个选项以便我们避免它。

作为一种解决方法,我构建了一个能够将查询传输到 ASP.NET MVC 控制器的 ASPX 页面。

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 中得到一个可解析的错误。

如果您希望将非 ASP.NET 错误重定向到您的自定义错误,您可以添加 httpErrors 部分。

【讨论】:

  • 我将这里提供的解决方案与stackoverflow.com/a/5536676/2310818 结合起来处理各种错误,无论是由于未知路由、未知控制器、未知操作、控制器操作返回的 HttpNotFound() 结果,以及控制器抛出的 HttpException。我实现了所有这些,同时根据错误代码的类型实现了正确的状态代码(404、500)。
猜你喜欢
  • 2018-04-30
  • 2012-01-09
  • 2011-01-29
  • 1970-01-01
  • 2018-07-04
  • 1970-01-01
  • 1970-01-01
  • 2011-09-15
相关资源
最近更新 更多