Better Custom Errors in ASP.NET
4/12/2008 8:12:06 PMTechnologies
- 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!
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.
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.
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