Better Custom Errors in ASP.NET

4/12/2008 8:12:06 PM

Technologies

  • ASP.NET
  • .NET Framework 2.0
  • IIS 6 / IIS 7

When an error occurs in your ASP.NET application you are given a nice browser yellow screen of death. This is fine during development but presenting your customers with such a wonderfully designed web page is probably not your idea of a great user experience. You need something more, you need custom error pages!

browser yellow screen of death

All is not lost, luckily ASP.NET provides a feature called CustomErrors that can be used to redirect a user to a more user friendly page that you have created.

It is very easy to set up. Just a few changes in the web.config and you are good to go.

   1:  <customErrors mode="On" defaultRedirect="ErrorMessage.aspx">
   2:  <customErrors>

Now when a really bad error has occured our nice user friendly error page gets served to our cutomer, letting them know all is well.

custom error page

Did I say luckily? What I really meant to say was that although ASP.NET custom error pages are dead simple to use it does some things that for a lot of people just aren't acceptable. To see what's going on lets first take a peek at the http response headers served when no ASP.NET custom errors are used. If you are using Firefox a simple tool called live http headers makes viewing http headers dead simple.

   1:  HTTP/1.x 500 Internal Server Error
   2:  Date: Wed, 26 Mar 2008 19:31:25 GMT
   3:  Content-Type: text/html; charset=utf-8
   4:  Content-Length: 7025
   5:  Connection: Close

See that HTTP/1.x 500 Internal Server Error? That is telling the client that something broke on the server. It is the right way to inform a client that an error has occured through http headers. Lets now take a look at the headers when using custom errors, starting with the client's request.

   1:  GET /General/TestError HTTP/1.1
   2:  Host: localhost:2971
   3:  Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
   4:  Accept-Encoding: gzip, deflate
   5:  Connection: keep-alive

So far so good, so what does the server send as a response?

   1:  HTTP/1.x 302 Found
   2:  Date: Wed, 26 Mar 2008 19:36:25 GMT
   3:  Location: /ErrorMessage.aspx?aspxerrorpath=/General/TestError
   4:  Content-Type: text/html; charset=utf-8
   5:  Connection: Close

After seeing a HTTP/1.x 302 Found a client says oh no! This files has been temporarily moved to the Location: /ErrorMessage.aspx?aspxerrorpath=/General/TestError I better try it at that location instead. It then sends another request.

   1:  GET /ErrorMessage.aspx?aspxerrorpath=/General/TestError HTTP/1.1
   2:  Host: localhost:2971
   3:  Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
   4:  Accept-Encoding: gzip, deflate
   5:  Keep-Alive: 300

And then the server responds . . .

   1:  HTTP/1.x 200 OK
   2:  Date: Wed, 26 Mar 2008 19:36:25 GMT
   3:  Content-Type: text/html; charset=utf-8
   4:  Content-Length: 561
   5:  Connection: Close

So you end up with a HTTP/1.x 200 OK letting the client know all is well, it found the content it was looking for. That isn't what happened though, an application error occured on the server. ASP.NET custom errors swept the error under the rug!

Covering up that an error has occured is a bad thing. It creates some extra requests, confuses bots, and can cause SEO related issues.

Now for many what custom errors are doing is an acceptable problem. The customer experience trumps the concerns of a measily browser or bot . . . and to that I say you are correct! However, with little effort you can get the best of both worlds.

Creating a Better Custom Errors HttpModule

HttpApplication provides an Error event that we can use to catch all unhandled errors inside of our HttpModule. The idea is to handle the unhandled exceptions that pass through our module by setting the http error code that is appropriate and then serving up our user friendly error page.

   1:  public class BetterCustomErrorsModule : IHttpModule
   2:  {
   3:      public void Dispose() { }
   4:   
   5:      public void Init(HttpApplication context)
   6:      {
   7:          context.Error += new EventHandler(Context_Error);
   8:      }
   9:   
  10:      void Context_Error(object sender, EventArgs e)
  11:      {
  12:          HttpApplication application = (HttpApplication)sender;
  13:          HttpContext context = application.Context;
  14:          HttpException httpException = context.Error as HttpException;
  15:   
  16:          if (httpException != null)
  17:              context.Response.StatusCode = httpException.GetHttpCode();
  18:          else
  19:              context.Response.StatusCode = 500;
  20:   
  21:          context.ClearError();
  22:          context.Server.Transfer("/BetterErrorMessage.aspx");
  23:      }
  24:  }

If the error that we are handling is an HttpException we use the error code it provides, if we are dealing with a different type of exception we are going to make the assumption that it is an HTTP 500 - Internal Server Error.

ClearError is called because we have handled the error and don't want it propogated. Server.Transfer is used because it halts the current execution of the request and starts to execute the new page specified. This means that no HTTP 302 temporary redirect is sent back to the client and instead the error page is just served directly.

To see this bad boy in action a small web.config change to the http modules is all that is needed. For IIS 6 this will be done in the httpModules config section.

   1:  <httpModules>
   2:      <add name="BetterCustomErrorsModule" type="MyApplication.BetterCustomErrorsModule" />
   3:  </httpModules>

If you are one of the lucky ones and get to use IIS 7 then your HttpModule will be set up differently.

   1:  <system.webServer>
   2:      <modules runAllManagedModulesForAllRequests="true">
   3:          <add name="BetterCustomErrorsModule" type="MyApplication.BetterCustomErrorsModule" />
   4:      </modules>
   5:  </system.webServer>

Now when an error occurs we once again see a nice friendly error page.

better custom errors

But this time the http header information we set in the http module is served.

   1:  HTTP/1.x 500 Internal Server Error
   2:  Date: Thu, 27 Mar 2008 16:27:52 GMT
   3:  Content-Type: text/html
   4:  Connection: Close

Making a Modular HttpModule

It's good practice to have a standalone and reusable HttpModule and currently the Better Custom Errors Module fails in that regard because it assumes the location of the friendly error page to be /BetterErrorMessage.aspx. However, I don't want to fry anyones brain or my own so turning this into a stand alone HttpModule will be a post for another day.

Update: Better Custom Errors in ASP.NET: Custom Configuration Section

Comments

Rosco

Rosco

2/9/2009 6:40:21 PM

Thanks! I used your HttpModule code to fix the issue, but modified it as below, so that it respects the settings in the section. Hope this helps someone. public void Context_Error(object sender, EventArgs e) { HttpApplication application = (HttpApplication)sender; HttpContext context = application.Context; System.Configuration.Configuration configuration = WebConfigurationManager.OpenWebConfiguration("~"); SystemWebSectionGroup systemWeb = (SystemWebSectionGroup)configuration.GetSectionGroup("system.web"); CustomErrorsSection customErrorsSection = systemWeb.CustomErrors; // If customerrors mode == off, then just let IIS handle it if (customErrorsSection.Mode != CustomErrorsMode.Off) { // If mode == on or its a remote request, we want to get the pretty page if (customErrorsSection.Mode == CustomErrorsMode.On || !context.Request.IsLocal) { HttpException httpException = context.Error as HttpException; // Log the actual cause Exception ex = httpException.GetBaseException(); // Log the actual exception here... string sURL = customErrorsSection.DefaultRedirect; if (httpException != null) { context.Response.StatusCode = httpException.GetHttpCode(); CustomErrorCollection customErrorsCollection = customErrorsSection.Errors; CustomError customError = customErrorsCollection[context.Response.StatusCode.ToString()]; if (customError != null) sURL = customError.Redirect; } else { context.Response.StatusCode = 500; } context.ClearError(); context.Server.Transfer(sURL); } } }

Evan

Evan

http://www.evanclosson.com
2/10/2009 3:17:07 PM

Thanks Rosco, looks like I need to clean up my comment rendering.

James Messinger

James Messinger

http://www.jamesmessinger.com
6/21/2009 7:22:04 PM

As of ASP.Net 3.5, this is no longer necessary. You can now use to make ASP.Net return the correct HTTP status code. More info here: msdn.microsoft.com/en-us/library/system.web.configuration.customerrorssection.redirectmode.aspx