ASP.Net MVC Custom Error Pages

I have recently spent quite a bit of time getting my ASP.Net MVC application to display errors to the user the way I want and this post outlines the steps I took to get there.  This includes general application errors, page not found errors and stupid user errors.  A basic understanding of ASP.Net and the MVC framework will be required to follow this post.  For reference my environment looks like this:

  • Windows Server 2008
  • IIS7 using the Integrated Pipeline
  • Visual Studio 2008
  • ASP.Net MVC Beta

If you have a different setup the details provided here might not work exactly as described, especially if you are running a lesser version of IIS like 5 or 6.  Upgrade, seriously.

CustomErrors Web.config Settings

Remember, ASP.Net MVC is just a new layer on top of the same old ASP.Net we know and love hate put up with.  The custom errors section of the Web.config was something we always had to set up for web forms projects so it seems like a good place to start for MVC.  Here's what my settings look like:

    <customErrors mode="On" defaultRedirect="~/Error/Unknown" />

Nice and simple.  All I'm saying here is that should anything crazy happen that my application doesn't deal with, redirect the user to my really bad error page.  The URL I provide is going to map to one on my MVC controllers which I'll get to shortly.

ASP.Net allows you to add rules for specific HTTP status codes but I'm going to ignore all that because I want these settings to be used as little as possible.  If you're interested in why there are already plenty of people out there talking about the issues with custom errors, e.g. this guy, that guy and another guy also.

ErrorController

In my application I have a controller dedicated to serving up application level errors.  Here's a simplified version:

    public class ErrorController : Controller

    {

        [AcceptVerbs(HttpVerbs.Get)]

        public ViewResult Unknown()

        {

            Response.StatusCode = (int)HttpStatusCode.InternalServerError;

            return View("Unknown");

        }

 

        [AcceptVerbs(HttpVerbs.Get)]

        public ViewResult NotFound(string path)

        {

            Response.StatusCode = (int)HttpStatusCode.NotFound;

            return View("NotFound", path);

        }

    }

The Unknown action is the target of my custom errors redirect in the Web.config.  The NotFound action is going to be used by the routing system which I will describe below.  Note that both actions set the HTTP status code to make sure our response is absolutely clear to the client.

There are intentionally few types of errors here because if the user is seeing these it probably means there is an improvement I can make to the application.  The views that these actions render are very generic and don't have enough context to be very useful to the user.  They may as well just say Oops!

IIS7 Detailed Errors

IIS7, trying to be helpful, likes to steal your custom errors pages and replace them with it's own:

IIS7 500 Internal Server Error

Annoyingly these errors will be swapped in whenever you set the HTTP status code to something interesting.  To crack down on this insubordination you need to open the Error Pages feature in IIS and click Edit Feature Settings on the right hand menu to get this dialog:

IIS7 Edit Error Pages Settings

You might be tempted to think that the Custom error pages option is what you should choose to display your custom error pages... but you'd be wrong.  These are IIS7 custom errors pages and something else entirely from what we are after.  Set the Detailed errors option to have IIS pass through whatever errors we serve up from ASP.Net.

Internet Explorer Friendly Errors

Internet Explorer, trying to be helpful like it's big brother IIS, wont display your error pages if they are smaller than 512 bytes.  By default IE has a setting called Show friendly HTTP error messages turned on and this causes your nice custom errors to be replaced with the standard canned reply:

IE Error

You can find the option to turn this off in IE under Tools, Internet Options, Advanced, Browsing, Show friendly HTTP error messages.  Unfortunately you can't set this for all of your users (or install Firefox for them) so just make sure that all of your error pages are larger than 512 bytes and you should be fine.

I find this one to be quite annoying during development when I only have stub pages in place.

Page Not Found

One of the most basic errors all sites will need to serve is the standard 404, page not found.  I could let the custom errors settings deal with this but as well as adding an extra redirect it also requires all requests to be mapped to ASP.Net in IIS, even those without a standard extension like .aspx.

Keeping things in code is my preference as it makes them easier to test and easier to move to new environments.  The MVC routing features let me deal with this nicely by adding a simple catch all route after all my standard application routes:

    routes.MapRoute("Default", "{controller}/{action}/{id}",

        new { controller = "Home", action = "Index", id = "" });

 

    routes.MapRoute("Catch All", "{*path}",

        new { controller = "Error", action = "NotFound" });

This maps any completely invalid routes to ErrorController.NotFound which we've already seen.  This happens in a single request, without any redirects, works with all URLs and doesn't require a special mapping in IIS.

Route Matched But Parameters Are Invalid

If you have controller actions that take parameters you need to take some additional steps with your routing.  An error that kept popping up for me early on was something like this:

The parameters dictionary does not contain a valid value of type 'System.Int32' for parameter 'id' which is required for method 'System.Web.Mvc.ActionResult Edit(Int32)' in 'Example.ProductController'. To make a parameter optional its type should either be a reference type or a Nullable type.

Ouch, what?!  To get rid of these errors you can add constraints to your routes using regular expressions, e.g.

    routes.MapRoute("Products - Edit", "Products/{id}/Edit",

        new { controller = "Products", action = "Edit" },

        new { id = @"\d{1,}" });

The last line specifies that the id parameter must be one or more digits an not contain any crazy stuff like letters or tilde symbols.  Should someone decide to be sneaky and type rubbish which the ID should go, they will pass right over this route and the catch all route will send them to the 404 page.

Errors In Controllers

Even once a request has successfully been mapped to an action there is plenty that could go wrong.  Here's a simple example where the requested product isn't found in the database:

    public ProductController : Controller

    {

        [AcceptVerbs(HttpVerbs.Get)]

        public ActionResult ViewDetails(int id)

        {

            var product = GetProductFromDB(id);

 

            if (product == null)

            {

                Response.StatusCode = (int)HttpStatusCode.NotFound;

                return View("NotFound");

            }

 

            return View("Details", product);

        }

One of the advantages MVC has over the web forms model is that you can delay choosing which view to render right until the last moment.  Here I am using a not found page tailored specifically for the products section of the site so that I can try and guess what the user was looking for and give them as much help as possible.

There are more advanced ways of catching errors in controller but they all basically have the same effect, rendering a different view.

Conclusion

That's all for today.  If you have any questions, comments or corrections I'd love to hear from you so leave a comment below.

27/01/2009 04:34 AM (UTC -08:00)