CustomErrors vs HttpErrors - A significant design flaw?

前端 未结 2 1178
孤城傲影
孤城傲影 2020-12-09 05:46

As we might know, from e.g. What is the difference between customErrors and httpErrors?, CustomErrors is a older way to define error pages in a web application but this appr

相关标签:
2条回答
  • 2020-12-09 06:14

    Just like you, I think ASP.NET shouldn't set TrySkipIisCustomErrors or may add an option so we can avoid it.

    As a workaround, I built an ASPX page that is able to transfer the query to an ASP.NET MVC Controller.

    ErrorHandler.aspx.cs

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

    Code behind

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

    Usage in 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>
    

    The behavior is correct on regular browser request : the error page is shown when there is an error and the error code is returned. It's also correct on an AJAX/Web API request : the error page is NOT returned. We get a parsable error in JSON or XML.

    You may add a httpErrors section if you want non-ASP.NET errors to be redirected to your custom errors.

    0 讨论(0)
  • 2020-12-09 06:30

    I've been struggling for several days to solve this problem and I think the only valid solution is the one posted here (An answer by Starain chen to the same question posted by @Alex on forums.asp.net):

    (I've slightly modified the code)

    The code

    Create a custom handle error attribute

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

    Use this custom handle error attribute in App_Start/FilterConfig.cs

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

    Handle the remaining exceptions in 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));
    }
    

    Create an 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();
        }
    }
    

    Create views

    Create views for the actions in ErrorController. (403.cshtml, 404.cshtml, 500.cshtml and General.cshtml)

    Why do I think this is the only valid solution?

    1. It handles both ASP.NET MVC and IIS-level errors (Assuming IIS7+ and integrated pipeline)
    2. It returns valid http status codes. (Not 302 or 200)
    3. It returns 200 OK if I navigate to the error page directly: I'd like to get 200 OK if I navigate to /error/404.
    4. I can adjust the error page content. (Using ViewBag.ErrorMessage)
    5. If the client makes an erroneous AJAX request and expects json data (within the actions of controllers) he/she will be served with a json data with the appropriate status code.
    0 讨论(0)
提交回复
热议问题