skip to content
Stylized letter A in teal with pink sparkle accents Khalid Abuhakmeh

ASP.NET MVC Custom Errors Are Broken... But Here Is The Fix

/ 5 min read

Custom error pages are one of the last development items on the creation of an ASP.NET application. During development, we want to see the yellow screen of death early and often to ensure users don’t see the error pages at all. I’ve personally added custom errors to many web applications, and even wrote about it in a previous post.

In a new application, I decided to take a different approach than described in my previous post. I was surprised that my previous approach would not work. This post will describe the initial approach, why it failed, and how to get what I wanted. It also is a culmination of 8+ hours of pure pain that I hope you can avoid.

The Broken Approach

When errors occur, I like to use the existing url and return the appropriate HTTP status code. To do this, I used the customErrors section as recommended by most in the ASP.NET space. Here is what the section looks like.

<customErrors mode="On"
defaultRedirect="~/errors"
redirectMode="ResponseRewrite">
<error statusCode="404" redirect="~/errors/404" />
<error statusCode="500" redirect="~/errors/500"/>
</customErrors>

Two things to note about the customErrors section above:

  1. The redirectMode is ResponseRewrite. This means the response should be hijacked in the case of a server error.
  2. We want to handle 404 and 500 errors

When the above is implemented in an ASP.NET MVC application, all we get is…

A BLANK WHITE SCREEN!!!

If you have received the same result, it is normal.

Why CustomErrors is Broken

The customErrors response rewrite functionality depends on Server.Transfer. The server transfer functionality was introduced in IIS 5.0 to eliminate the need to do a client side redirect to retrieve the contents of another .asp file. Classic ASP files and ASPX pages were all built to be executed as a self contained unit.

When you use the Transfer method, the state information for all the built-in objects are included in the transfer. This means that any variables or objects that have been assigned a value in session or application scope are maintained. In addition, all of the current contents for the Request collections are available to the .asp file that is receiving the transfer. – MSDN

This is not how MVC is built to function, instead it relies on the pipeline to return a view result. We are getting a blank white page when using the MVC urls is because the customErrors feature is looking to execute a page.

The Fix

It took hours to realize why something so integral to ASP.NET was not working, but once I did figure it out, with the help of Justin Rusbatch, I realized it was completely broken. We stumbled on a really good solution on StackOverflow by Red Taz. He had implemented his own HttpModule to handle all errors and properly route the errors through the MVC pipeline.

/// <summary>
/// Enables support for CustomErrors ResponseRewrite mode in MVC.
/// </summary>
public class ErrorHandler : IHttpModule {
private HttpContext HttpContext { get { return HttpContext.Current; } }
private CustomErrorsSection CustomErrors { get; set; }
public void Init(HttpApplication application) {
System.Configuration.Configuration configuration = WebConfigurationManager.OpenWebConfiguration("~");
CustomErrors = (CustomErrorsSection)configuration.GetSection("system.web/customErrors");
application.EndRequest += Application_EndRequest;
}
protected void Application_EndRequest(object sender, EventArgs e) {
// only handle rewrite mode, ignore redirect configuration (if it ain't broke don't re-implement it)
if (CustomErrors.RedirectMode == CustomErrorsRedirectMode.ResponseRewrite && HttpContext.IsCustomErrorEnabled) {
int statusCode = HttpContext.Response.StatusCode;
// if this request has thrown an exception then find the real status code
Exception exception = HttpContext.Error;
if (exception != null) {
// set default error status code for application exceptions
statusCode = (int)HttpStatusCode.InternalServerError;
}
HttpException httpException = exception as HttpException;
if (httpException != null) {
statusCode = httpException.GetHttpCode();
}
if ((HttpStatusCode)statusCode != HttpStatusCode.OK) {
Dictionary<int, string> errorPaths = new Dictionary<int, string>();
foreach (CustomError error in CustomErrors.Errors) {
errorPaths.Add(error.StatusCode, error.Redirect);
}
// find a custom error path for this status code
if (errorPaths.Keys.Contains(statusCode)) {
string url = errorPaths[statusCode];
// avoid circular redirects
if (!HttpContext.Request.Url.AbsolutePath.Equals(VirtualPathUtility.ToAbsolute(url))) {
HttpContext.Response.Clear();
HttpContext.Response.TrySkipIisCustomErrors = true;
HttpContext.Server.ClearError();
// do the redirect here
if (HttpRuntime.UsingIntegratedPipeline) {
HttpContext.Server.TransferRequest(url, true);
}
else {
HttpContext.RewritePath(url, false);
IHttpHandler httpHandler = new MvcHttpHandler();
httpHandler.ProcessRequest(HttpContext);
}
// return the original status code to the client
// (this won't work in integrated pipleline mode)
HttpContext.Response.StatusCode = statusCode;
}
}
}
}
}
public void Dispose() {
}
}

Once the HttpModule is in the application, we just needed to register it in our configuration.

<!-- IIS7+ -->
<system.webServer>
<modules>
<add name="ErrorHandler" type="Foo.Bar.Modules.ErrorHandler" />
</modules>
</system.webServer>

This was the best solution we found because it still used the MVC infrastructure we setup.

Conclusion

It’s amazing that something integral to ASP.NET is broken for ASP.NET MVC, what I consider to be Microsoft’s flagship web development framework. We will be moving this code into a NuGet package shortly, so that we can use it in all of our ASP.NET MVC applications, and so that other developers can take advantage of the great solution that Red Taz has come up with. Next time you see a white screen for a custom error page, just know you aren’t crazy.

Related Posts