Handling Custom Errors Correctly in ASP.NET MVC

As the .NET library (and indeed, to a point, the MVC library) is a one glove fits all, there are often many ways of doing the same thing. A prime example of this is handling errors and custom error pages. 

In the days of WebForms, the general approach was straight forward - stick a custom errors section in the web.config:

<customErrors mode="On" defaultRedirect="~/generic-error" redirectMode="ResponseRewrite">
    <error statusCode="500" redirect="~/generic-error" />
    <error statusCode="404" redirect="~/page-not-found" />
</customErrors>

The key here is the redirect mode being set to ResponseRewrite - meaning that no redirect (301, 302) is performed before the error page is shown. Great and all, but try this with MVC and you will quickly find that this doesn't work! (Unless you use an actual file on disk as your error page, as ResponseRewrite uses Server.Transfer behind the scenes).

So what's the solution?

Well, either you can stick with custom errors in the web.config, and drop the redirect mode attribute, but you will end up with an HTTP 302 response followed by your error HTTP response which isn't ideal. Alternatively, we can handle all errors at application level by using the Application_Error method in out Global.asax. This allows us full control over customisation, and we can also pass routedata such as exceptions to our custom pages. For example, this is our Application_Error:

protected void Application_Error(object sender, EventArgs e)
        {
            Exception exception = Server.GetLastError();

            //Log Error Here - removed for brevity

            HttpException httpException = exception as HttpException;
            RouteData routeData = new RouteData();
            IController errorController = new Controllers.ErrorController();
            routeData.Values.Add("controller", "Error");
            routeData.Values.Add("area", "");
            routeData.Values.Add("ex", exception);

            if (httpException != null)
            {
                switch (httpException.GetHttpCode())
                {
                    case 404:
                        Response.Clear();
                        routeData.Values.Add("action", "PageNotFound");
                        Server.ClearError();
                        errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
                        break;
                    case 400:
                        Response.Clear();
                        routeData.Values.Add("action", "BadRequest");
                        Server.ClearError();
                        errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
                        break;
                    default:
                        Response.Clear();
                        routeData.Values.Add("action", "ServerError");
                        Server.ClearError();
                        errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
                        break;
                }
            }
            //All other exceptions should result in a 500 error as they are issues with unhandled exceptions in the code
            else
            {
                routeData.Values.Add("action", "ServerError");
                Server.ClearError();
                // Call the controller with the route
                errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
            }
        }

The Application_Error method will capture all unhandled errors that occur in the application. Briefly, we check to see if the exception is that of an HTTPException, and if it is, check the status code, and choose our error page based on that (you can extend the case list with any http status code you want to handle specifically). If the error is not an HTTPException, then we simply return a server error (HTTP500), as that describes what has happened correctly.

Our error controller looks like this:

    [AllowAnonymous]
    public class ErrorController : Controller
    {
        public ActionResult PageNotFound()
        {
            Response.StatusCode = 404;
            return View();
        }

        public ActionResult ServerError(Exception ex)
        {
            Response.StatusCode = 500;
            return View(ex);
        }

        public ActionResult CatchAllUrls()
        {
            throw new HttpException(404, "The requested url " + Request.Url.ToString() + " was not found");
        }
}

There are two things you may notice here - firstly, we are accepting an object Exception in our ServerError ActionResult. If you take a look back at the Application_Error, we are adding an entry into our route data called "ex" with the value of the current exception. You can now display the exception and stack trace on your custom error page (for debugging, not publicly obviously). The second thing is the CatchAllUrls ActionResult that we haven't talked about yet. We can add a catchall rule in our route table pointed at this ActionResult. you could just return the PageNotFound ActionResult, but I am interested in logging all of my 404's, so I am raising an exception which will push the code path through my Application_Error for logging.

My corresponding route for catching all urls looks like this, and is the last route in my route table to catch any urls that don't match routes:

routes.MapRoute("CatchAllUrls", "{*url}", new { controller = "Error", action = "CatchAllUrls" });

And that's about it - this will handle all application errors. It, of course, doesn't take into account IIS level errors.

 



Tagged: ASP.NET, MVC, Error Handling,
Categorised: ASP.NET MVC,
By:
On: