Error Handling in ASP.NET MVC 3: Part 3 – HandleErrorAttribute and HttpApplication.Error

In the previous post I covered the Custom Errors and HTTP Errors error handling features. You can configure both of these in various ways, including specifying that your own error page should be shown when there is an error. What you do not get is complete control. Today you are going to see how you can gain that control by manually handling errors. As with the last post, I am continuing to concentrate on how 500 Internal Server Error errors can be handled.

This is Part 3 of a series of posts on the subject of adding error handling to an ASP.NET MVC 3 Web application. The index to this series can be found here.

The Basics of Manually Handling Errors

In an ASP.NET MVC Web application, there are two basic approaches to manually handling errors: the HandleErrorAttribute filter and the HttpApplication.Error event. With both of these, there are a number of desired behaviours that are important for the resulting response to be correct.

  • A non-404 error should be logged.
  • No redirect should occur.
  • The response needs to be cleared to remove any headers and/or content that have already been written to the response before the exception was thrown. (As will be discussed later in the post, there are actually situations where clearing the response is unnecessary because the error page is rendered in a new request.)
  • The response status code needs to be set to the appropriate error code.
  • TrySkipIisCustomErrors needs to be set to prevent IIS replacing your error response with its own.
  • The exception that triggered the error needs to be marked as handled, so that ASP.NET does not pass it on to the next error handling feature.

Exactly how these steps are performed depends on the way you are manually handling errors.

Manual Error Handling using the HandleErrorAttribute Filter

Action filters are the mechanism used in ASP.NET MVC for declaratively adding additional behaviour to an action method. Action filters are implemented as attributes, and these attributes can be added to an action method, to a controller, or to all controllers in the site. There are four types of action filter, but the one of interest here is the exception filter type. It is invoked when an unhandled exception is thrown from within the MVC framework, either from an action method or from any action filters that get invoked during the handling of the request.

The ASP.NET MVC framework comes with an exception filter called HandleErrorAttribute. If you create an MVC 3 Web application and then look at the site’s Global.asax.cs file, you will see that an instance of this exception filter gets added to the global filter collection at application start-up:

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new HandleErrorAttribute());
}

By adding the filter to the global filter collection, it gets applied to all controllers and therefore all action methods. Note that earlier version of ASP.NET MVC do not have this global filter collection; you have to individually apply the filter to the controllers in your Web site.

While this filter is the exception filter that is applied by default, there is nothing to stop you creating your own exception filter for the site to use instead. If you need to add additional behaviour to the HandleErrorAttribute class, you can just derive from it and override the OnException method. (Deriving is something I have done in the demo application because I needed to be able to switch the HandleErrorAttribute filter on and off.)

The HandleErrorAttribute filter can be customized in two useful ways. Firstly, you can specify what type of exceptions it should handle. By default this type is System.Exception, so it potentially handles all types of exception. Secondly, you can specify the view it should render as the response. The following is an example of how to specify these customizations:

new HandleErrorAttribute
{
    View = "SomeView",
    ExceptionType = typeof (SomeException)
}

Notice that you specify a view rather than an action method. By default it renders a view called “Error” – this view gets automatically added when you create an MVC Web site – but in the demo application I have instead specified that it should render a view called “HandleErrorAttributeMvcErrorPage”. This view can be found in the /Views/Shared directory.

You can go to the MVC section of the ASP.NET Codeplex Web site and view the source code for the HandleErrorAttribute class to see how it works. The bulk of the logic is in the OnException method.

  1. If Custom Errors is not enabled, the method exits without taking any action.
  2. If the exception thrown is an HttpException and invoking GetStatusCode() on it does not return 500, the method exits without taking any action.
  3. If the type of the exception thrown is not the type that this filter should handle, the method exits without taking any action.
  4. The exception is used to populate a view model, along with the controller and action names from the original request.
  5. A view result is created using the data in the view model, one that will display the view the filter has been specified to show.
  6. The exception is marked as handled, the response is cleared, and the response status code is set to 500.
  7. The TrySkipIisCustomErrors property on the response is set to true.
  8. The created view result is returned so that it can be executed by the MVC framework.

By only handling HttpExceptions that have an HTTP status code of 500, the HandleErrorAttribute allows errors like 404 Not Found errors to remain unhandled, and so be dealt with in some other way.

Looking back at the previous section at the list of desired behaviours when manually handling errors, you can see that the HandleErrorAttribute takes care of all of the behaviours except the logging of the exception. You could add this by deriving from the HandleErrorAttribute class and overriding the OnException method. In that method you would call the base OnException method and then log the exception if that base method marked the exception as handled:

public class DerivedHandleErrorAttribute : HandleErrorAttribute
{
    public override void OnException(ExceptionContext filterContext)
    {
        base.OnException(filterContext);
        
        if (context.ExceptionHandled)
        {
            // Log filterContext.Exception in some way.
        }
    }
}

This is the approach described by Atif Aziz on Stack Overflow. An alternative approach is that taken by Ivan Zlatev, where you create an exception filter that logs the exception if it has been marked as handled. Like the HandleErrorAttribute filter, this logging filter is also registered in global.asax as a global filter, but it is registered first. The exception filters get invoked in reverse order of their registration, so the logging filter gets invoked after the HandleErrorAttribute filter has had a chance to mark the exception as handled.

The next experiment shows the HandleErrorAttribute filter in action.

Experiment: HandleErrorAttribute with Custom Errors on
Custom Errors – Mode On
Manual Error Handling – 500 error handling Handle Error Attribute
All other settings Default values
Error link to click Action method exception on GET request

You should get the following custom error page:

37569444-hea-with-custom-errors-on

The page has the correct status code and there is no redirect.

If you look back at the filter’s implementation details, you will see that Custom Errors needs to be enabled for it to handle the exception. So you could try turning Custom Errors off.

Experiment: HandleErrorAttribute with Custom Errors off
Custom Errors – Mode Off
Manual Error Handling – 500 error handling Handle Error Attribute
All other settings Default values
Error link to click Action method exception on GET request

You should get the following Custom Errors detailed YSOD:

37569584-hea-with-custom-errors-off

The HandleErrorAttribute checks whether Custom Errors is on or off by calling the HttpContext.IsCustomErrorEnabled method. That method determines if Custom Errors is enabled or not from the current Mode setting of Custom Errors and whether the request being handled is local or remote. In this way, if Custom Errors is in its default Mode of RemoteOnly, you get the user-friendly custom error page for remote requests and the developer-friendly YSOD for local requests.

The ASP.NET in ASP.NET MVC

As described in the first post in this series, the ASP.NET MVC framework code is only one component in the IIS request-handling pipeline for a site, given that the pipeline consists of various managed ASP.NET and native modules. This means that it is not only ASP.NET MVC code that executes to handle a given request. Given that the HandleErrorAttribute filter can only handle exceptions throw from within that framework, it is clear that such a filter can never be the only error handling solution in an MVC Web site.

The following experiment illustrates this point by asking you to click the BeginRequest exception error link. The link causes an exception to be thrown from the BeginRequest event.

Experiment: HandleErrorAttribute with an external exception
Custom Errors – Mode On
Manual Error Handling – 500 error handling Handle Error Attribute
All other settings Default values
Error link to click BeginRequest exception

You should get the basic YSOD screen. The exception was thrown from outside the MVC framework so it was not handled by the HandleErrorAttribute filter. Instead it was handled by Custom Errors.

Clearing the Response

You can do a final experiment with this filter to see a shortcoming it has. This experiment will use a different error link.

Experiment: Clearing of the response
Custom Errors – Mode On
Manual Error Handling – 500 error handling Handle Error Attribute
All other settings Default values
Error link to click Failed file download

The error screen that you’ll see will depend on the browser you are using. In Chrome I get a blank screen. In Firefox I get a message about how the image “… cannot be displayed because it contains errors”. In IE9 I get the error page displayed as text rather than be rendered as HTML.

The problem with the response is that the type of the content returned does not match the declared Content-Type in the header. The content is a Web page and a Web page should have a Content-Type value of ‘text/html’, but the response has a value of ‘image/jpeg’. You can see this for yourself if you use your browser’s developer tools to view the response headers.

The error link simulates what happens when an exception is thrown during a request for a file that is not a Web page. If you look at the FailedFileDownload action method in the ErrorGenerationController class, you will see that it returns a FileStreamResult with a stream that throws an exception as soon as the MVC framework tries to read from it. The key is that by then the MVC framework has already set the Content-Type header of the response to ‘image/jpeg’.

If you look back at the implementation details of the HandleErrorAttribute filter, you will see that it clears the response. It does this like so:

filterContext.HttpContext.Response.Clear();

You would think that the Clear method clears both the headers and the content of the response, but in fact it only clears the content. To get the error page displaying correctly, you have to do the following:

filterContext.HttpContext.Response.ClearHeaders();
filterContext.HttpContext.Response.ClearContent();

Unfortunately you cannot fix this by deriving a class from the HandleErrorAttribute filter. You could instead duplicate that filter class and fix the issue in the copy.

The HandleErrorAttribute Filter in Summary

As it is the error handling mechanism that comes with the ASP.NET MVC framework, the HandleErrorAttribute exception filter is the standard way to deal with unhandled exceptions and HttpExceptions with a 500 status code in an ASP.NET MVC Web application. It will display a friendly MVC error page that has the correct status code and without redirecting to that page. However, out-of-the-box the filter does not log the errors it handles and it does not clear the response headers. You can easily fix the logging issue, but you would have to take a different approach to fix the response-clearing issue. Furthermore, the HandleErrorAttribute exception filter can never be the only error handling mechanism in an MVC Web site since the filter only deals with particular exceptions that have been thrown from within the action method invoker code in the MVC framework.

Manual Error Handling using the HttpApplication.Error Event

As described in the first post in this series, the reqeust-handling pipeline for a given Web site includes a list of events that are raised in turn as a request is processed. You can hook into these events by adding handlers to the site’s HttpApplication class, or by creating an HTTP module and adding it to the pipeline using the site’s Web.config file. The event of interest here is the Error event, which is invoked if an unhandled exception gets thrown. You can hook into this event by adding a listener using the += operator, or by adding a method to the HttpApplication-derived class that has the following signature:

public void Application_Error(object sender, EventArgs e)

This method will get automagically added as a listener to the Error event.

It is likely that the listener you add to the Error event is one that could be reused across multiple Web sites. To facilitate this, you can create an HttpModule that registers one of its methods as a listener to that event. Karl Seguin has a post that describes this approach and its benefits. I decided to take this approach for the demo application. The HttpModule I created is called ErrorHandlerHttpModule, and you will find it in the MvcHelpers directory. The following is the skeleton of that class:

public class ErrorHandlerHttpModule : IHttpModule
{
    public void Init(HttpApplication context)
    {
        context.Error += Application_Error;
    }
    
    private static void Application_Error(object sender, EventArgs e)
    {
        // Error handling code goes here.
    } 
    
    public void Dispose() 
    {
    }
}

To add this module to the ASP.NET pipeline, I added the following markup to the demo application’s Web.config file:

<system.web>
    <!-- Additional configuration elided. -->
    <httpModules>
        <add
            name="ErrorHandlerHttpModule"
            type="MvcErrorHandling.MvcHelpers.ErrorHandlerHttpModule, MvcErrorHandling" />
    </httpModules>
</system.web>
 
<system.webServer>
    <!-- Additional configuration elided. -->
    <modules runAllManagedModulesForAllRequests="true">
        <add
            name="ErrorHandlerHttpModule"
            type="MvcErrorHandling.MvcHelpers.ErrorHandlerHttpModule, MvcErrorHandling"/>
    </modules>
</system.webServer>

Notice how the module has to be added in two separate places. The system.web addition is for IIS 6 and the Visual Studio Development Server, and the system.webServer addition is for IIS 7. This allows you to run the demo application either through IIS 7 or through the Visual Studio Development Server.

The module contains quite a lot of code that is required by the demo application to make it configurable. Ignoring that code, you will see that I start by obtaining the exception that was thrown:

var exception = HttpContext.Current.Server.GetLastError();

I then try to cast that exception as an HttpException. If the cast succeeds, I read its HTTP status code value:

var httpException = exception as HttpException;
...
var statusCode = httpException.GetHttpCode();

This status code is used for the response status code. If the exception is not an HttpException then I consider it an unhandled exception with a status code of 500.

I now want to show an MVC custom error page. This is not straightforward to do, because this code is outside of the MVC framework and I need a way to get back into it. There are at least four ways to do this: the view result approach, the controller execute approach, the MvcHttpHandler approach, and the TransferRequest approach. The view result approach only requires an MVC view, while the others require an action method and a view. I will describe each of these approaches in a moment.

For all but the TransferRequest approach, I have to then clear the error:

HttpContext.Current.Server.ClearError();

This step is important as if I did not clear the error it would remain as unhandled and would get passed to Custom Errors. My custom error page would then get overridden by whatever Custom Errors is configured to show.

The View Result Approach

The following experiment shows you this approach in action.

Experiment: The View Result Approach
Manual Error Handling – 500 error handling Application Error Handler
Manual Error Handling – App error handler response View Result
All other settings Default values
Error link to click Action method exception on GET request

You should get the following custom error page:

37570697-httpapperror-viewresult

It has the correct 500 error response code and there is no redirect. The code to create this response can be found in the Application_Error method in the ErrorHandlerHttpModule class. If you were using this approach, you could add your logging code to that method.

With this approach, I begin creating the view result response by clearing the response and setting its status code:

HttpContext.Current.Response.ClearHeaders();
HttpContext.Current.Response.ClearContent();
HttpContext.Current.Response.StatusCode = statusCode;

Now I want to create and execute a ViewResult instance, but to do that I need a controller context. And to create that controller context I need a controller instance and a RouteData object that has a controller name added to it. It turns out that, while these are required, they do not actually get used so I can use a fake controller and a ‘whatever’ controller name:

var routeData = new RouteData();
routeData.Values.Add("controller", ErrorControllerRouteName);
 
var controllerContext = new ControllerContext(
    new HttpContextWrapper(HttpContext.Current),
    routeData,
    new FakeController());

This technique of using a fake controller comes from the NotFound MVC project, which I will be discussing in the next post in the series.

Now I can create a ViewResult instance that is initialized to display the desired view. Any values required by that view can be added to the ViewResult’s ViewBag:

var viewResult = new ViewResult {ViewName = "ManualErrors"};
// Setting of values on its ViewBag here.
viewResult.ExecuteResult(controllerContext);
HttpContext.Current.Server.ClearError();

This approach is similar to how the HandleErrorAttribute filter works, since a ViewResult is executed directly rather than an action method being invoked. However, it feels like a bit of a hack to me.

The Controller Execute Approach

The following experiment shows you this approach in action.

Experiment: The Controller Execute Approach
Manual Error Handling – 500 error handling Application Error Handler
Manual Error Handling – App error handler response Controller Execute
All other settings Default values
Error link to click Action method exception on GET request

You should get the same custom error page as in the previous experiment, also with a 500 error status code and no redirect.

This approach requires that you create an action method for the error page you want to display. To execute this action, you need a RouteData instance that specifies it. If you look at the Application_Error method in the ErrorHandlerHttpModule class, you can see how I do this:

var routeData = new RouteData();
routeData.Values.Add("controller", ErrorControllerRouteName);
routeData.Values.Add("action", actionName);
 
var requestContext = new RequestContext(
    new HttpContextWrapper(HttpContext.Current),
    routeData);
    
var controllerFactory = ControllerBuilder.Current.GetControllerFactory();
 
var controller = controllerFactory.CreateController(
    requestContext,
    ErrorControllerRouteName);
    
controller.Execute(requestContext);
 
HttpContext.Current.Server.ClearError();

If you take this approach, you can log the exception and perform actions like clearing the response and setting its status code either in the module’s handler method or in the action method. If you chose the latter option, you can get the exception that was thrown using Server.GetLastError(). Regarding the status code, you have options as to how to set it. You could create an action method for each of the basic types of error and set the status code in that method. (For example, you could create an action method for handling server errors and have it set the response status code to 500.) Alternatively, you could determine the status code to use from within the action method by checking the exception that was thrown. I use the route data to pass the status code to the action method, but this is only really because of the particular needs of the demo application.

This approach feels like less of a hack than the view result approach, but it could have issues: I once saw a comment – probably on Stack Overflow – that this approach does not work if the original request fails request validation. You can try this scenario out for yourself using the demo application and the Request validation error link.

Experiment: Request validaton exception
Manual Error Handling – 500 error handling Application Error Handler
Manual Error Handling – App error handler response Controller Execute
All other settings Default values
Error link to click Request validation error

You should get the expected custom error page. This suggests that there is no issue with request validation failures.

The TransferRequest Approach

The following experiment shows you this approach in action.

Experiment: The TransferRequest Approach
Manual Error Handling – 500 error handling Application Error Handler
Manual Error Handling – App error handler response Transfer Request
All other settings Default values
Error link to click Action method exception on GET request

You should get the expected custom error page, with a 500 error status code and no redirect.

You saw in the previous post how the Server.Transfer method does not work for MVC URLs. There is, however, an alternative method called Server.TransferRequest that does. It was introduced in .NET 3.5 and it only works in IIS version 7.0 or higher, and only if the Web site’s application pool is running ASP.NET in Integrated Mode. Thomas Marquardt wrote a blog post that includes the following description of the method:

It is essentially a server-side redirect. When invoked, a new request context is created, called the child request, and it goes through the entire IIS 7.0/ASP.NET integrated pipeline. The child request can be mapped to any of the handlers configured in IIS, and it’s [sic] execution is not different than it would be if it was received via the HTTP stack. The parent request jumps ahead in the pipeline to the end request notification, and waits for the child request to complete. When the child request completes, the parent request executes the end request notifications and completes itself.

The key here is that the client never knows that a new request has been made; no redirect is issued to the client.

If you look at the Application_Error method in the ErrorHandlerHttpModule class, you will see that I create the URL to redirect to and append the error status code to it as a query string parameter. I also require that the method should discard any existing query string and form parameters, and use the “GET” HTTP method when making the request:

HttpContext.Current.Session["exception"] = exception;
 
var url = "~/Error/ManualErrors?statusCode=" + statusCode;
HttpContext.Current.Server.TransferRequest(url, false, "GET", null);

You cannot use Server.GetLastError in your custom error page to access the exception that was thrown because a new request is made for the error page, and Server.GetLastError only returns the last error for the current request. So to log the exception, you have to either log it in the module’s handler method or you have to pass the exception via session state to the action method code. I chose to take the latter approach. Also, I chose to pass the status code to the action method via a query string parameter, but you could determine the status code value in the action method from the passed exception.

Since Server.TransferRequest only works with IIS version 7.0 or greater in Integrated mode, it does not work with the Visual Studio Development Server. You can check this for yourself by pressing Ctrl+F5 in Visual Studio and repeating the last experiment. You should see a detailed YSOD. What actually happens is that an exception is thrown by Server.TransferRequest with the message, “This operation requires IIS integrated pipeline mode.” Because this exception gets thrown, the ASP.NET framework passes the original exception on to Custom Errors to be handled.

There is an additional issue: TempData does not work with Server.TransferRequest, so you cannot use TempData to transfer data from the parent request to the child request.

The MvcHttpHandler Approach

The following experiment shows you this approach in action.

Experiment: The MvcHttpHandler Approach
Manual Error Handling – 500 error handling Application Error Handler
Manual Error Handling – App error handler response Mvc Http Handler
All other settings Default values
Error link to click Action method exception on GET request

You should find that a detailed YSOD is returned, with a 500 error status code and with no redirect. What has actually happened is that an exception has been thrown by the MvcHttpHandler class, one with the message”‘HttpContext.SetSessionStateBehavior’ can only be invoked before ‘HttpApplication.AcquireRequestState’ event is raised.” Because this exception gets thrown, the ASP.NET framework passes the original exception on to Custom Errors to be handled. However, if you repeat this experiment using the Visual Studio Development Server, you will find that you do get the expected custom error page.

What this approach does is create an instance of the class that handles MVC page requests – MvcHttpHandler – and then pass it the URL of the error page to display. If you look at the Application_Error method in the ErrorHandlerHttpModule class, you will see how I have done this:

var path = HttpContext.Current.Request.Path;
 
HttpContext.Current.RewritePath("~/Error/ManualErrors");
HttpContext.Current.Session["statusCode"] = statusCode;
 
IHttpHandler handler = new MvcHttpHandler();
handler.ProcessRequest(HttpContext.Current);
 
HttpContext.Current.RewritePath(path);
HttpContext.Current.Server.ClearError();

I pass the status code to the error page using session state, but you could determine the status code value within the error page action method after you have invoked Server.GetLastError to get the exception.

Combining Server.TransferRequest and MvcHttpHandler

If you want to use Server.TransferRequest but you still want to support running the site via the Visual Studio Development Studio (and I think you would want to), you can combine the two approaches. This is done by using the HttpRuntime.UsingIntegratedPipeline property to detect which method to use:

if (HttpRuntime.UsingIntegratedPipeline)
{
    // TransferRequest approach.
}
else
{
    // MvcHttpHandler approach.
}

Errors Within Errors

It is possible that the custom error page you are showing could itself throw an exception. You can simulate this scenario for any of the custom error pages in the demo application if you set the Error page rendering mode to Throw Exception. The following are the important points to be aware of if this happens:

  • Generally the result will be whatever Custom Errors is configured to show. If the error page exception is thrown from within Application_Error, it will be the original exception and not the new exception that is passed to Custom Errors.
  • If Custom Errors is configured to show a custom error page and that page throws an exception, it will show a basic YSOD instead.
  • If you are using the HandleErrorAttribute filter and it throws an exception, it is that exception and not the original exception that is passed to Custom Errors.
  • If you are handling errors in Application_Error and are using Server.TransferRedirect to show the error page, IIS will try the transfer redirect each time the error page throws an exception, up to a total of ten times. If those ten attempts fail then IIS falls back to showing the built-in HTTP Errors error page, detailed or basic according to the current HTTP Errors configuration.

There are various options for how you can deal with exceptions thrown by error pages, and they depend on how you choses to handle errors in your application. For now, I only want to make you aware of the problem. In the last post of this series, I will be considering how the various error handling features I have discussed can be combined into strategies for handling errors in an ASP.NET MVC Web application. When I do that, I will also cover how each strategy handles exceptions thrown by error pages.

Finally

This post has covered two ways of manually handling errors in ASP.NET MVC Web applications: the HandleErrorAttribute filter and the HttpApplication.Error event. In the next post, we will move on to ways of handling 404 Not Found errors.

7 thoughts on “Error Handling in ASP.NET MVC 3: Part 3 – HandleErrorAttribute and HttpApplication.Error

  1. Alex says:

    >> HttpContext.Current.Session["exception"] = exception; How to get it in the action method? System.Web.HttpContext.Current.Session doesn’t contain it inside action ErrorController.ServerError

  2. middleengine says:

    If I select the TransferRequest Approach experiment, then the exception gets set on the session object and I am able to access it from the error action method. I just double-checked this by debugging the code.

  3. Alex says:

    1) I downloaded ErrorEventStrategy solution2) modified Application_Error in global.asaif (HttpRuntime.UsingIntegratedPipeline) { System.Web.HttpContext.Current.Session["ex"] = exception; // inserted // For IIS 7+ HttpContext.Current.Server.TransferRequest(errorRoute, false, "GET", null); } …..3) Modified ErroControllerpublic ActionResult ServerError() { Exception ex = (Exception)System.Web.HttpContext.Current.Session["ex"]; Exception ex2 = (Exception)Request.RequestContext.HttpContext.Session["ex"]; InitializeResponse(); …..4) Starting app, clicking on "Action method exception on GET request"When i’m getting to ErroController.ServerError both variables ex and ex2 are null.

  4. Pawel says:

    In the first experiment under The View Result Approach I am getting a status code of 200 instead of 500. I noticed this a few times on other experiments as well.

  5. Pawel says:

    Maybe it is a problem with the frame mode, because it also shows HTTP status code of 200 for the Controller Execute manual handler method, but the same link when opened in a new window and reviewed in Chrome’s dev tools shows proper 500 status code.

  6. kicknwing says:

    In the github code for Application_Error, there’s a line towards the top that reads "exception = If(exception Is Nothing, exception, exception.GetBaseException())" – I don’t understand that line. Can you explain?

  7. middleengine says:

    kicknwing: If the exception instance is not null, I want to replace it with the innermost exception on that object. The innermost exception is normally the most useful one. You can choose yourself how to deal with the exception instance; I just chose this approach.

Leave a comment