Dustin Horne

Developing for fun...

Dynamic Custom Error Pages in ASP.NET MVC 2

Recently, I had to implement a dynamic custom 404 error page in an ASP.NET MVC2 website.  My first instinct was to turn CustomErrors mode on and setup a 404 Error, but this failed to return my custom error page in a shared hosting environment and instead displayed the default IIS 404 page.  I'll walk you through fixing this in just a minute, but for now I want to show you how to create the custom 404 page.

Creating the View

The first step in setting up a dynamic custom 404 is to create your view.  In my case I just added a new View to the project called 404 which created 404.aspx in my root directory (I did not add it to the Views folder, but you can if you'd like).  I kept the 404 page outside of the default Views folder as it wasn't part of my essential application.  My 404 View used my public site Master Page which has a dynamic menu.

Creating the Controller

The next step in setting up the custom 404 page was creating a controller to handle it.  My site uses many custom routes, including one for {category}/{urlSlug} so I loaded the View based on the file path and executed this controller when needed for the other pages.  In my case I will never have any static pages so my {category}/{urlSlug} route will catch everything that is not specifically mapped to other sections (such as my Admin area).
For my 404, I just used my HomeController and added a PageNotFound method.  The PageNotFound method is extremely simple.  It is responsible only for setting the StatusCode of the response to 404 and returning the 404.aspx page that I created:
[HttpGet]
public ViewResult PageNotFound()
{
     Response.StatusCode = 404;
     return View("~/404.aspx");
}


Mapping the 404 Route

In my case I can have any number of dynamic routes based on a supplied category and urlSlug (remember my {category}/{urlSlug} route?).  Because of this, I need to specifically create a route for my 404 page.  This route needs to be above all other routes to ensure it gets processed:
routes.MapRoute(null, "404", new { controller = "Home", action = "PageNotFound" });
Now the 404 page can be accessed via http://exampledomain.com/404.  This is perfect as it makes it easy for me to access from my dynamic content method.

Triggering the 404 Page

The 404 page is triggered by simply throwing an HttpException from code.  In my case, an attempt is made to load a content page from the database.  If no content page exists, an HttpException is thrown.  For the sake of this article I'm going to sidetrack a bit and teach you how to check whether a view exists and throw an HttpException if it doesn't.  In my example, under Views I've created a folder called "Tools".  The Tools folder is meant to contain static Views.  Now, I create a route for Tools/{view} to determine which view page to load.  I've created a basic Tools route below my 404 route, but above my {category}/{urlSlug} route to ensure that it is processed:
routes.MapRoute(
                null, // Route name
                "tools/{view}", // URL with parameters
                new { controller = "Home", action = "LoadToolPage", view="weather" }
            );
As you can see, any Url matching /tools/xxx will pass the value of {view} (in this case xxx) to the LoadToolPage's view parameter.  I've assigned a default view value of "weather" which will load a page displaying the current weather conditions.  This can be whatever you like, or it can be an optional parameter if you want to load a tools index page.
Now I create the LoadToolPage method in the Home controller with a string parameter called view.  For the sake of brevity I'm not validating the view string, only showing you how to check and see if the view exists.  If it doesn't, throw a 404 Exception which will eventually trigger our custom 404 page:
[HttpGet]
        public ViewResult LoadToolPage(string view)
        {
            string viewPath = "~/Views/Tools/" + view + ".aspx";

            ViewEngineResult result = ViewEngines.Engines.FindView(ControllerContext, viewPath, null);
            if (result.View != null)
            {
                return View(viewPath);
            }
            else
            {
                throw new HttpException(404, "Not Found");
            }
        }

Configuring Web.Config

There are a couple steps to configuring the web.config file to make sure the 404 page is executed.  The first is to make sure the customErrors attribute is set.
     <customErrors mode="On" redirectMode="ResponseRedirect">
          <error statusCode="404" redirect="~/404" /> 
     </customErrors>  
At this point you would normally brush your hands off and be done, however the customErrors section does not redirect all requests for MVC websites since the requests could be for static content.  This is the gotcha to be aware of when setting up your custom 404 pages.  In order to ensure all 404's get handled by your custom 404 page, you will need to configure the <httpErrors> element.  The <httpErrors element sits inside the <system.webServer>  </system.webServer> element of your web.config file.  If it doesn't exist, go ahead and create it, then configure your <httpErrors> element as follows:
   <httpErrors defaultPath="~/404" defaultResponseMode="ExecuteURL" existingResponse="Auto">
      <remove statusCode="404"/>
      <error statusCode="404" path="~/404" responseMode="ExecuteURL" />
    </httpErrors>   
    

And that's it! Your custom 404 page should now display for any missing pages.