Error Handling in ASP.NET MVC 5: Part 1 – Beginnings and the Demo Application

Some years ago I had to add error handling to an ASP.NET MVC Web application. I quickly ran into what must be many .NET developers’ experience of the process: growing confusion as to how the various features of MVC, ASP.NET and IIS are conspiring to keep me from completing the task. This tangle is memorably portrayed by secretGeek:

When an error happens, a bunch of different code modules will go to war. The victorious code module will tear out the entrails of all those who oppose it, and throw them in the visitor’s face. You’ll get a different error handler depending on what version of IIS you have, what version of MVC you’re using, whether you’ve deployed in debug or release, whether you’re visiting locally or remotely, whether it’s sunny or raining.

Yep, that sounds about right.

This post is the first in a series in which I try to bring some predictability to the process, with the emphasis being on practical demonstrations as to how the various ASP.NET and IIS error handling features work and interact with each other. My focus is on handling errors in an ASP.NET MVC Version 5 Web application that is running on IIS 7.0 or IIS 7.5 using .NET Version 4.5, but nearly all of the information applies equally to earlier versions of ASP.NET MVC. My intended audience is developers who are new to the topic, but more experienced .NET developers should find useful information throughout the series.

The index to this series can be found here.

The Requirements for Error Handling

From my experience, the two most common types of error are 404 Not Found and 500 Internal Server Error. If a client requests a non-existent page on a Web site, the correct response would be a 404 error. If a client makes a valid request but some unexpected problem occurs on the server, the correct response would most likely be a 500 error. I will be concentrating on these two types of error in this series of posts because I wish to somewhat restrict the scope of what I cover, and because the handling of these errors represents the full range of error handling features available in an ASP.NET MVC Web application.

When a 404 error occurs, I believe that the server should respond in the following way:

  • it should create an appropriate response, such as a user-friendly 404 page;
  • it should set the HTTP status code in the header of the response to 404;
  • it should render the response as part of the request that failed rather than redirecting the client to the error page.

When a 500 error occurs, I believe that the server should respond in a similar way:

  • it should log the error;
  • it should create an appropriate response such as a user-friendly 500 page;
  • it should set the HTTP status code in the header of the response to 500;
  • it should render the response as part of the request that failed rather than redirecting the client to the error page.

There are a number of logging solutions available, including ASP.NET Health Monitoring, log4net and ELMAH, and any of these can be used successfully; I’m only interested in where the logging should be done and what error information is available at that point. You will notice that I have excluded logging from the handling of 404 errors. You could log these errors but I expect that broken links would normally be found by regularly running a suitable tool on the site or by checking Google Webmaster Tools.

The appropriate response to an error depends on the request. Normally the response should be an error page, but if the request is an AJAX request then an alternative form of response is almost certainly required by the client. The design of your error pages is not something that I will be covering in this series, but if you are interested then there is some good advice from Jeff Atwood regarding 404 error pages.

Setting the right error status code in the response header is important. When a search engine is crawling the Web, it may follow a link to a page on your site that does not exist. If your response is to return a 404 error page with a HTTP status code of 200 OK, something Google calls a soft 404 error, it would appear to the search engine that the requested page does exist and can be indexed. The correct error status code is also important when making AJAX requests: the HTTP status code is used by the client to determine if an AJAX request succeeded or not.

You can display an error page in one of two ways: you can show the error page as the response for the request that failed, or you can issue a 302 redirect to it. Consider the scenario of a user browsing your Web site. If you respond with the error page, the URL in their browser does not change, but if you issue a 302 redirect then the URL does change (to that of the error page). The best practise is to not redirect. If a request has been made for a non-existent page on your site and you do redirect to the site’s 404 page, it would look to a search engine that the page has temporarily moved and it is the page it is being redirected to that does not exist; that is obviously wrong.

For 404 errors there is another possible response: you could issue a 302 redirect to, say, the site’s home page. However, this post by Ross Dunn cautions against doing so.

I have seen arguments that for 500 errors you could redirect to the error page in order to preserve the failing URL’s ‘juice’ until the page is fixed. I find it very hard to believe that a search engine would de-link a page simply because it returns a 500 error, not unless that page returns that same response for a significant period of time. Also, by redirecting you are in effect lying to the search engine about what has occurred since it is the original page that is failing and not the redirected page. I am no SEO expert, but from what I have read it seems best to be straightforward with search engines.

Error Handling and the IIS Pipeline

There are a number of error handling features available within an ASP.NET MVC Web application. To introduce them, and to show how they fit into the IIS request-processing pipeline, I will now describe how IIS 7 handles HTTP requests in Integrated Mode.

All HTTP and HTTPS requests to a Windows server get picked up by the kernel mode device driver called the HTTP protocol stack, or HTTP.sys. In the case of a request for an ASP.NET MVC Web site, HTTP.sys forwards the request to that site’s worker process.

A worker process is an instance of w3wp.exe, and each is associated with an application pool. When you create a Web site in IIS Manager, you specify the application pool that the site will run in. You can run each Web site in its own application pool or you can have multiple sites running in a given application pool. To isolate the sites from each other in an application pool, each site is run in its own application domain.

The application domain for a given site contains a pool of instances of the ASP.NET application class for the site. That class derives from the HttpApplication system class, as declared in the site’s global.asax file:

public class SomeApplicationClass : HttpApplication
{
    ...
}

When a request is received for a site, an application class instance is retrieved from the site’s application pool. The site’s request-handling pipeline uses this instance to process the request. The pipeline consists of an ordered list of native and managed modules. The native modules are written in C++ while the managed modules are ASP.NET modules. When you run an application pool in IIS 7 in Integrated Mode, the site has a single pipeline that contains a mix of native and managed modules. (If you were to run the site’s application pool in Classic Mode, there would be two separate pipelines.) To process the request, a series of events get raised. You can hook into these events by adding handlers to the site’s application class, or by creating an HTTP module and adding it to the pipeline using the site’s Web.config file.

One of the native modules in the request-handling pipeline is the CustomErrorModule. This module implements IIS error handling, that is, error handling outside of the managed ASP.NET modules. It can be configured by adding an httpErrors element to the system.webServer section in the site’s Web.config file. Consequently, I will refer to this feature as HTTP Errors.

When an unhandled exception gets thrown from a managed module, the HttpApplication.Error event gets raised. If you add a handler to this event, you can interrogate and maybe handle the error. If you do not handle the error, the ASP.NET framework invokes a different error handling feature called Custom Errors. This feature can be configured by adding a customErrors element to the system.web configuration section in the site’s Web.config file.

The response for a request to an ASP.NET MVC Web application normally gets generated by an instance of the MvcHandler class. You associate this handler with pages of your site when registering the site’s routes:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        "Default",
        "{controller}/{action}/{id}",
        new {controller="Home", action="Index", id=UrlParameter.Optional});

    // More routes here.
}

This step registers these routes with a managed module called the UrlRoutingModule. At the appropriate moment, this module tries to match the request to one of the registered routes. If it finds a match, it executes the associated IRouteHandler. By default, that route handler creates an instance of MvcHandler which in turn generates the response.

Internally, the MvcHandlerclass includes a few error handling features that you can hook into. The most notable of these is the HandleErrorAttribute error filter. It allows you to deal with unhandled exceptions thrown from within the code that finds and executes the correct action method on a given controller.

Once the MvcHandler has created the response, the pipeline completes execution and the response is returned to HTTP.sys for dispatch.

From the above description you can see that there are multiple locations in IIS where errors can be handled:

  • HTTP Errors (i.e., CustomErrorModule)
  • Custom Errors
  • HttpApplication.Error event
  • HandleErrorAttribute

Which features get to process an error depends on where in the site’s code the error occurs. If an exception gets thrown from within an action method, it will be the HandleErrorAttribute feature that first processes it. If the exception remains unhandled, any handlers for the HttpApplication.Error event get invoked. If the exception still remains unhandled, the Custom Errors feature is invoked and it handles the exception. Finally, HTTP Errors may replace the response generated by Custom Errors with its own response.

These four features are the key error handling features that I will be covering in this series. I will begin by covering them from the point of view of handling 500 Internal Server Error errors, and then I will cover them from the point of view of handling 404 Not Found errors.

The MvcErrorHandling Demonstration Application

If you have previously tried experimenting with the error handling features in ASP.NET and IIS, you will know that it is a chore to do so. To remedy this, I have created a simple ASP.NET MVC 5 demonstration Web application that can be dynamically configured as to how it handles errors.

app

You can use this application to see for yourself how the various features behave. This series of posts includes a number of experiments that you can perform with the application. Each experiment explains how the demo application should be configured and what the expected result should be.

Requirements

  • You need to be running a Windows OS that includes IIS version 7.0 or 7.5, such as Windows 7 or Windows Server 2008 R2.
  • You need to have installed Microsoft Visual Studio 2013 and ASP.NET MVC Version 5. If you are using an Express version of Visual Studio, use Visual Studio Express 2013 for Web.
  • You need to be using a modern browser such as a recent version of Chrome, Firefox, or Internet Explorer.

Setup

  1. Get the source code from the project page at GitHub. You can download it as a zip file, or you can use Git to clone the repository.
  2. Run Visual Studio 2013 in Administrator mode and open the solution file Mvc5ErrorHandling.sln.
  3. Build the solution by pressing Ctrl+Shift+B.
  4. Right-click the application project in the Solution Explorer window and select the Properties option.
  5. In the Properties window, select the Web tab and tick the Use Local IIS Web server option. Set the Project Url value to http://localhost/Mvc5ErrorHandling.
  6. Click the Create Virtual Directory button.
  7. Enter the URL http://localhost/Mvc5ErrorHandling in your browser. You should now be viewing the demo application.

If you want to check how the error handling features behave when the application is running under the Visual Studio Development Server, you can press Ctrl+F5 in Visual Studio 2013 you need to select the User Visual Studio Development Server option in the Web tab of the Properties window for the Mvc5ErrorHandling project. Once you have selected that option and saved the project, you should be able to run the site by pressing Ctrl+F5 in Visual Studio.

Another hosting option is IIS Express from within Visual Studio, but I have found that it sometimes behaves differently to IIS 7+ when using Server.TransferRequest and when handling 403 errors. Given that, for the purposes of this series of posts, I recommend that you do not use it when running the Web projects in the series.

A Quick Tour

The demo application consists of a single page that is split into two sections.

  • The upper section is the Settings section and it consists of a number of Web.config file settings. To change these, set the values as required and then click the Update button. Notice that all settings have a default value. You can restore all the default values at once by clicking the Reset button.
  • The lower section is the Error Trigger Links section and it contains a number of links that you can click to trigger particular errors.

When you click on an error link, you are going to be interested in three things:

  • the page that is returned;
  • its HTTP status code;
  • whether or not a redirect occurred.

Out of the box, the demo application is configured to display the response in an iframe below the Error Trigger Links section. This is the behaviour when selecting Frame for the Link open mode setting. You can easily see the page that is returned and its status code, but you cannot see if a redirect occurred. If you instead select New Window for the Link open mode setting, the response will be displayed in a new tab. This allows you to see if a redirect occurred, as the URL displayed in that tab will be for the error page rather than the requested page.

You may prefer to use the browser developer tools to view the complete request time line and so view response codes and redirects in that way. In fact, I cannot get the response status code for the error links that perform a POST request and so you have to use those tools to view that value.

Caveats

The demo application is unusual in that it edits its own Web.config file. When that file is updated, the server notices and reloads the site’s application domain so that subsequent requests are handled according to the new state of Web.config. This process is quick; I am able to make a request right after updating Web.config and see the altered behaviour. This may not be true for all users, and I would be grateful for feedback if problems occur.

The application expects the sections of its Web.config file that relate to error handling and application settings to have particular values. If you were to manually edit the Web.config file, you could cause the application to be unable to parse the information. If this happens, just reload the home page and the application should reset itself.

Finally, when using the Frame option I have to issue two requests. The first is done so that I can get the response status code, while the second is done so that the response will be correctly displayed in the iframe. This is only something you need to be aware of if you start debugging the demo application.

Finally

This post has been about framing the problem and introducing the demo application. Next time we will start using the application to see how the Custom Errors and HTTP Errors features work.

Leave a comment