MVC Friendly Error Pages

in Software Engineering

I recently found myself looking to create a way of handling friendly error pages for an MVC project. Having done this several times in the past I know how inconsistent and problematic this can actually be. So I decided to go away and spend time looking into all the different alternatives to come up with the best way to fulfill my requirements. This post will be a summary of what I found and what solution I eventually used.

Requirements

Firstly before looking into what existed I needed to know what I actually wanted, so I listed out my requirements for the error pages functionality and their configuration. These boiled down to 4 major requirements:

  1. Pages that cause an error should have the friendly error page returned on the original request and not return a redirect to the friendly error page. This has a few advantages leading to its reason for being a requirement:

    • The actual url that the request was made on returns the correct HTTP status. This allows monitoring software (such as NewRelic) to correctly identify on what urls you are seeing errors.
    • Not returning a 301 or 302 HTTP response also helps with having browsers act correctly to error pages. Often if you redirect to your friendly error pages some browsers will cache that redirect for a short time meaning that the error response appears to be present for longer than expected.
    • You can't just refresh the page to try again. Whilst this is a slight irritation for developers, it could actually be quite an issue for real users who may not realize the url they are on has changed.
    • Can pass on SEO value from the page that resulted in an error to the resulting error page url.
  2. Dynamic content can be added via Controllers and Actions like other pages on your site. This allows for the friendly error page to fit in with your site using the same layouts and partials as the rest of the site. Without this there would likely be a need for copy and paste of code.

    However this highlights a sub-requirement: a configurable fallback friendly error page. If the friendly error page is dynamic it could fall victim to error-ing itself and therefore a static fallback is required.

  3. The ability for developers to still be able to receive detailed error messages from ASP.Net or IIS when in debug.

  4. Friendly error page configuration should be possible in one place. So the friendly error page that is set up for 404s and 500s should be configured in the same place, as should the friendly error pages for static content or dynamic content. This requirement was the least of my three requirements and more of a nice to have.

What already exists?

Once I had my collection of requirements I started going through the existing ways of generating friendly error pages to see if any met my requirements and how to configure them.

  1. HandleErrorAttribute

    The HandleErrorAttribute or another similar custom attributes are attributes that can be added onto controllers/actions or globally to intercept exceptions being thrown by controller actions and then respond with a friendly error page.

    The problems with using this approach for the above requirements are:

    • The built in HandleErrorAttribute only uses views rather than controllers actions for creating the friendly error page.

    • Doesn’t work for static resources as they don't route via a controller action.

    • Doesn’t work for requests that don’t hit a route - E.g. Not Found.

  2. customErrors

    customErrors is the ASP.Net way of handling friendly error pages. These are configured in the customErrors section of the web.config and information on how to use this can be found online.

    The problems with using this approach for the above requirements are:

    • By default Redirects to another url and if set to not redirect ('redirectMode' to 'ResponseReWrite') it uses a Server.Transfer that bypasses MVC routing.

    • Difficult to introduce a static fall-back error page for failing error pages as by default ASP.Net sets a flag to tell IIS to not handle error pages. It is supposed to be possible to control this flag in code, but a bug in (at least) MVC 5 stops the flag from being listened to.

    • Doesn’t handle static resources.

  3. httpErrors

    httpErrors is similar to customErrors, but is the friendly error page functionality provided by IIS. Again this is configured in web.config and information on how to use this can be found online.

    The problems with using this approach for the above requirements are:

    • As mentioned above by default a flag set by MVC causes http error pages to be skipped.

    • You can set http errors to ignore this flag, but that causes detailed error pages from ASP.Net to be lost.

    • No fall-back for failing error page possible as IIS intercepting is the final point at which errors can be handled.

  4. Global.asax - Custom code

    Global.asax is part of ASP.Net that you can add application level code to hook into certain events. Again the information on how to use this can be found online.

    The problems with using this approach for the above requirements are:

    • Doesn’t work with static resources

    • No built in fall-back and attempting to fall-back to customErrors or httpErrors was inconsistent and unreliable

My solution

So after lots of searching I found that you could accomplish all the requirements I had by creating a HttpModule with custom code to hook into the EndRequest event, then check for and handle errors with an error page configured how you'd like. Unfortunately this is highly customly written and not an out of the box solution, but could be extended to be configured (as I have in my real world use). Hopefully the basics of the code I used will be useful to someone.

It is also worth noting that in-order for this to work with all urls you must have runAllManagedModulesForAllRequests set to true in your web.config. This is considered by some to be an unreasonable performance burden for static resources, so that can be balanced on a case by case basis for a project. In my case it was already turned on for other required functionality.

The HttpModule code:

public class MyHttpModule: IHttpModule  
{
  public void Init(HttpApplication application)
  {
    application.EndRequest += Application_EndRequest;
  }

  private void Application_EndRequest(object source, EventArgs e)
  {
    ...
  }
}

web.config change to add for IIS 6 (and some versions of IIS Express and VS debugger)

<system.web>  
  <httpModules>
    <add name="MyHttpModule" type="MyHttpModule"/>
  </httpModules>
</system.web>  

web.config change to add for IIS 7+

<system.webServer>  
  <modules>
    <add name="MyHttpModule" type="MyHttpModule"/>
  </modules>
</system.webServer>  

The code for handling generating a friendly error page from a controller and action:

public void ErrorPageFromControllerAction(HttpContextBase context, string controllerName, string actionName)  
{
  context.Response.Clear();

  RouteData routeDate = new RouteData();
  routeData.Values["controller"] = controllerName;
  routeData.Values["action"] = actionName;

  RequestContext requestContext = new RequestContext(context, routeData);

  IController controller =
    ControllerBuilder.Current.GetControllerFactory().CreateController(requestContext, controllerName);
  controller.Execute(requestContext);
}

The code for handling generating a friendly error page from a static url:

public void ErrorPageFromUrl(HttpContextBase context, string url)  
{
  context.Response.Clear();
  context.Response.AddHeader("Content-Type", "text/html");
  context.Server.Execute(url);
}

In theory the url for the last snippet could also be a dynamic file using aspx, but that would be advised against in case it too errored leaving you without a friendly error page.

The header being added in the last snippet is to resolve a bug where sometimes the correct header is not set for static html or aspx files when returned via Server.Execute

I hope this post has been of some use to you and thanks for reading.