ASP.NET MVC Custom Error Handling Application_Error Global.asax?

178,615

Solution 1

Instead of creating a new route for that, you could just redirect to your controller/action and pass the information via querystring. For instance:

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

Then your controller will receive whatever you want:

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

There are some tradeoffs with your approach. Be very very careful with looping in this kind of error handling. Other thing is that since you are going through the asp.net pipeline to handle a 404, you will create a session object for all those hits. This can be an issue (performance) for heavily used systems.

Solution 2

To answer the initial question "how to properly pass routedata to error controller?":

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

Then in your ErrorController class, implement a function like this:

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

This pushes the exception into the View. The view page should be declared as follows:

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

And the code to display the error:

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

Here is the function that gathers the all exception messages from the exception tree:

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

Solution 3

I found a solution for ajax issue noted by 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");
    }
}

Solution 4

I struggled with the idea of centralizing a global error handling routine in an MVC app before. I have a post on the ASP.NET forums.

It basically handles all your application errors in the global.asax without the need for an error controller, decorating with the [HandlerError] attribute, or fiddling with the customErrors node in the web.config.

Solution 5

Perhaps a better way of handling errors in MVC is to apply the HandleError attribute to your controller or action and update the Shared/Error.aspx file to do what you want. The Model object on that page includes an Exception property as well as ControllerName and ActionName.

Share:
178,615

Related videos on Youtube

aherrick
Author by

aherrick

http://twitter.com/andrew_herrick http://andrewherrick.com Run, code, slick.

Updated on July 08, 2022

Comments

  • aherrick
    aherrick almost 2 years

    I have some basic code to determine errors in my MVC application. Currently in my project I have a controller called Error with action methods HTTPError404(), HTTPError500(), and General(). They all accept a string parameter error. Using or modifying the code below. What is the best/proper way to pass the data to the Error controller for processing? I would like to have a solution as robust as possible.

    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
    aherrick almost 15 years
    When you say "be careful of looping" what exactly do you mean? Is there a better way to handle this type of error redirect (assuming it WAS a heavily used system) ?
  • andrecarlucci
    andrecarlucci almost 15 years
    By looping I mean when you have an error in your error page, then you would be redirected to your error page again and again... (for instance, you want to log your error in a database and it is down).
  • Asbjørn Ulsberg
    Asbjørn Ulsberg over 14 years
    Redirecting on errors goes against the architecture of the web. The URI should remain the same when the server responds the correct HTTP status code so the client knows the exact context of the failure. Implementing HandleErrorAttribute.OnException or Controller.OnException is a better solution. And if those fail, do a Server.Transfer("~/Error") in Global.asax.
  • Chris
    Chris almost 11 years
    @asbjornu - If an entity is not found, it may be perfectly acceptable to redirect rather than return a 404 error, as long as you set the HTTP response code correclty, e.g. to 301 (permanent redirect) using Response.RedirectPermanent(...)
  • Asbjørn Ulsberg
    Asbjørn Ulsberg almost 11 years
    @Chris, It's acceptable, but not best practice. Especially since it's often redirected to a resource file that is served with an HTTP 200 status code, which leaves the client to believe that everything went okay.
  • Chris
    Chris almost 11 years
    @asbjornu - That's not correct :( In fact, when you use Response.RedirectPermanent(...) the server will return an HTTP 301 ("moved permanently") redirect response containing the URL, which asks the client to request the new URL. It's this second call which (correctly) returns an HTTP 200 status code for the page that is then served up. If you use Response.Redirect(...) the same occurs, but with an HTTP 302 ("moved temporarily") response code. (If using Google Chrome Dev Tools, switch 'Preserve Log upon Navigation' in the Network tab footer to see this in action).
  • Asbjørn Ulsberg
    Asbjørn Ulsberg almost 11 years
    @Chris, I am fully aware of the existence of RedirectPermanent(), which HTTP status code it emits and so forth. What I point out is that redirecting is moving the focus away from the original resource that emits a 500 status code to another resource that might or might not emit a 500 status code. More often than not, what you're redirecting to emits a "200 OK", which is not OK. Doing a 302 to a 500 is fine, but that just doesn't happen, at least not on Microsoft's stack.
  • Jeroen K
    Jeroen K almost 11 years
    I had to add <httpErrors errorMode="Detailed" /> to the web.config to make this work on the server.
  • Julian Dormon
    Julian Dormon about 10 years
    While implementing this I got the following error: 'System.Web.HttpRequest' does not contain a definition for 'IsAjaxRequest'. This article has a solution: stackoverflow.com/questions/14629304/…
  • Rafael Herscovici
    Rafael Herscovici almost 9 years
    How will you handle a 404 error then? since there is no controller/action designated for that?
  • Nikwin
    Nikwin almost 9 years
    The accepted answer includes 404s. This approach is only useful for 500 errors.
  • Rafael Herscovici
    Rafael Herscovici almost 9 years
    Maybe you should edit that into your answer. Perhaps a better way of handling errors sounds pretty much like All Errors and not 500 only.
  • Steve Harris
    Steve Harris about 6 years
    This is the best way IMO. Exactly what I was looking for.
  • burkay
    burkay about 6 years
    @SteveHarris glad it helped! :)
  • Thulasiram
    Thulasiram over 4 years
    Working Fine. Thanks :)
  • Prabhat Sinha
    Prabhat Sinha over 2 years
    I'm using this approach but some error not display in error page iny idea about it but it goes to error page also.
  • burkay
    burkay over 2 years
    I would put breakpoints to the beginning of Application_Error method and to the line starting with "@if(" in the view code and then inspect if (i) execution really hits the Application_Error method, (ii) Model in view code is not null and contains an exception object and (iii) debugging is enabled. Btw, as far as I remember, breakpoints on views only work if view code is not changed since last compilation.