Saturday 27 March 2010

Ektron and Web Application Projects (Part 3)

In this series of posts I’ve described a method of keeping your code and the required Ektron workarea separated.  This method essentially turns the ‘drop-in compiled workarea’ feature request on it’s head, by dropping your compiled code into the work area.  It’s the same result, just a slightly difference process (that you can do right now!):

  • In Part 1, I discussed why you might want to develop your website outside of the default Ektron workarea and gave a brief overview of your options.
  • In Part 2, I described how to organise the VS solution and configure IIS for use with Ektron workarea.

In this final part, I’ll describe how to combine your code with the Ektron workarea for final deployment.

Leveraging the Post-Build Event

The key to the whole process is the Post-Build Event in the clientproject.Website Web Application (see Part 2).  We can use this to copy your compiled code into the workarea website using a set of xcopy (or similar)commands.  To edit the Post-Build event right click on the clientproject.Website project and click ‘View Properties’ it will be on the ‘Build Events’ tab.  There’s a handy editor that let’s you know the values of various ‘macros’ for your project.  These macros are resolved automatically before the commands are executed and mean you don’t have to hard code absolute locations into your project.

Post-Build Event Editor in VS2008

These should include any css or js that your templates need, an example entry into the Post-Build Event could be:

   1: "$(SolutionDir)3rd Party\Tools\CodebehindRemover\CodebehindRemover" /i="$(ProjectDir)." /o="$(SolutionDir)web\." /e="ascx,aspx,master,asmx,ascx,svc,htm,html" 
   2: xcopy "$(ProjectDir)js" "$(SolutionDir)Web\js" /E /I /R /D /C /Y
   3: xcopy "$(ProjectDir)ui" "$(SolutionDir)Web\ui" /E /I /R /D /C /Y
   4: xcopy "$(ProjectDir)video" "$(SolutionDir)Web\video" /E /I /R /D /C /Y
   5: xcopy "$(ProjectDir)Xml" "$(SolutionDir)Web\Xml" /E /I /R /D /C /Y
   6: xcopy "$(ProjectDir)Xslt" "$(SolutionDir)Web\Xslt" /E /I /R /D /C /Y 
   7:  
   8: xcopy "$(ProjectDir)App_Browsers\*.*" "$(SolutionDir)Web\App_Browsers" /I /R /D /Y
   9: xcopy "$(ProjectDir)Configs\*.config" "$(SolutionDir)Web\Configs" /I /R /D /Y
  10: xcopy "$(ProjectDir)Configs\*.config.*" "$(SolutionDir)Web\Configs" /I /R /D /Y
  11: xcopy "$(ProjectDir)bin\*.dll" "$(SolutionDir)Web\bin" /E /I /R /D /C /Y
  12: xcopy "$(ProjectDir)bin\*.pdb" "$(SolutionDir)Web\bin" /E /I /R /D /C /Y
  13: xcopy "$(ProjectDir)bin\*.xml" "$(SolutionDir)Web\bin" /E /I /R /D /C /Y
  14: xcopy "$(ProjectDir)App_GlobalResources\*.resx" "$(SolutionDir)Web\App_GlobalResources" /I /R /D /Y 

You’ll notice that I’ve not included any of the .Net UI files (aspx, ascx, asmx, ashx, etc)  within the xcopy statements.  That’s because one of the implementation differences between Web Applications and Web Sites is that WAPS refer to their codebehind file using the codebehind page directive attribute, and websites use codefile.  Usually, when you compile (or precompile) a WS or WAP project those references get ignored as there’s no code file necessary.  However, when you’re trying to get ASP.Net to use a WAP aspx in a website you’ll get a compilation error. 

Fortunately, you don’t need a code file for the site to work – only for the ‘inherits’ attribute to be point towards your WAP page class.  So you can use a simple script/command line utility to remove any codebehind or codefile attributes as part of the Post-Build processing.  I’ve attached the source code for a basic command line application that I wrote to do this (imaginatively named CodeBehindRemover!) , the usage for the app is on line 1 of the code sample above.

With the post-build event in place, when you build your Web Application Project it will automatically be deployed into the vanilla workarea website.

Tip: If your IIS is locking your website files and preventing deployment you can add ‘iisreset /stop’ into the Pre-Build event and ‘iisreset /restart’ at the end of the Post-Build event

Attachment: Code Behind Remover

Wednesday 24 March 2010

Ektron and Web Application Projects (Part 2)

In Part 1, I discussed why you might want to develop your website outside of the default Ektron workarea and gave a brief overview of your options.

This time around, I’m going to show how to configure your Visual Studio Solution and projects and IIS to get you started.

You will need:

  1. A standard Ektron cms400min workarea website (I usually just call mine /Web – and copy directly from a fresh cms400min install!)
  2. A Web Application Project (clientproject.Website)
  3. A ‘3rd Party’ folder which will contain subfolders for the project dependencies binaries (this includes Ektron) and any helper utilities
  4. A ‘Virtual Directories’ folder (that isn’t included in the VS solution).  Move the uploadedFiles, uploadedImages, assets and privateAssets folders from the /web folder into here.  This will stop VS from rescanning the folder each time there’s a change.  If you don’t want to do this then you can set the hidden flag on just those folders (not their children) by right clicking and selecting properties.  I prefer the clean separation approach as the uploaded files and images are data and the asset files are generated artefacts.
  5. Optional: Class libraries containing business and helper classes (clientproject.Website.Support)
  6. All of the projects live in solution folder (imaginatively named clientproject) which also contains the solution file (also called clientproject) and a strong naming key.

The folder structure should look like this:

image

This may look complicated but it allows us to easily use source control (such as TFS or Subversion) to allow multiple developers to work on a solution and is Best Practise.

In keeping with the best practise handling of 3rd Party libraries:

  1. Move all of the files from ‘/Web/bin’ into ‘/3rd Party/Ektron’
  2. In Visual Studio, right click on the /Web website and select ‘Add References’
  3. In the dialog box, click' ‘Add’ and browse the /3rd Party/Ektron and select all of the dll’s
  4. Click Ok.

This will create ‘.refresh’ files in the ‘/Web/bin’ folder which can safely be checked in to any version control system.

Configuring IIS for the Workarea Website

The IIS configuration can be done now so we can begin to configure Ektron via the workarea.  This is a relatively simple procedure:

  1. Create a new Website (I prefer a format like local.cms.clientproject.dev.companydomain local as it’s a local developer version, cms as the workarea is present and the .dev.companydomain gives the opportunity to expose the environment externally via DNS).
    1. Associate the new website with the /web project.
    2. Set up the website Application Pool (I use IIS 7)
      1. Use the Integrated Pipeline
      2. Check that it’s running as the Network Service user
      3. Disable ‘pings’ (this will make debugging much easier!)
    3. Ensure that the web.config contains all of the handler mappings from the IIS 7 example configuration file (Usually found in C:\Program Files\Ektron\CMS400vXX\CommonFiles\IIS7Web.config)
  2. Create a ‘Virtual Directory’ (not an application) for each of the folders within /Virtual Directories names exactly as they would usually appear as folders in the workarea ie:
    • assets
    • PrivateAssets
    • uploadedImages
    • uploadedFiles

Validate the Workarea Website Modifications

The workarea website should pretty much be complete now from a infrastructure configuration point of view. but you will need to amend the following web.config settings:

  1. The Ektron.DbConnection database connection string
  2. The sessionState connection string – if your storing session in SQL Server
  3. The WSPath in appSettings should read ‘http://local.cms.clientproject.dev.companydomain/workarea/ServerControlWS.asmx’ (and you should be able to view that page as well)
  4. Any other settings (email servers, etc)

You should also be able to run SearchConfigUI (remember to right click ‘Run As Administrator’ in Windows Server 2008 or Windows 7).  Define meaning full catalogue names and check that the Assets and Private Assets directories have been correctly located (within the Virtual Directories folder).

You should now be able to log into the website by visiting ‘http://local.cms.clientproject.dev.companydomain/cmslogin.aspx’ and using the builtin account the was configured when Ektron was installed.

Setting the Solution the Build Order

It’s probably a good idea at this stage to configure any Code Analysis rules on the clientproject.Website and clientproject.Website.Support projects and to configure the dependencies and set the assemblies to be strongly named.  You should ensure that build order of the solution goes something like:

  1. clientproject.Website.Support
  2. clientproject.Website
  3. /Web

You can control this by setting project dependencies either implicitly by adding a reference to the clientproject.Website.Support project into the clientproject.Website project or by using the ‘Project Dependencies’ dialog (checking the build order in the ‘Build Order’ tab on the same screen).

Depending on the level on integration with the workarea you need, you can configure an IIS website to point at your Web Application project for lightweight testing of the your code.  Simply follow the instructions for configuring IIS but point the website at the /clientproject.Website folder.  This means you can quickly test your code, without IIS having to load all of the Ektron libraries.

At this point you can start to build your Ektron front end within the clientproject.Website and by copying some basic configuration from the /Web project (such as connectionStrings and appSettings). However, to deploy our web application into the Ektron workarea website we’re going to need some custom build steps, which I’ll cover in Part 3.

Tuesday 23 March 2010

Diagnosing an Ektron eSync Relationship

Ektron’s eSync service is a great product when it’s working but getting the initial configuration right can be a bit fiddly.  I’ve compiled a quick diagnostic procedure to resolve the most common issues, as well as some of the more common issues.
Note: The Ektron Windows Service is usually in: C:\Program Files\Ektron\EktronWindowsService30. After any configuration change restart the Ektron Windows Service on every node in the relationship.
  1. Restart the Ektron Windows Service on every node within the relationship
  2. Ensure that the Ektron license is valid for every database within the relationship
  3. Ensure that you can access http://<DOMAINNAME>/workarea/ServerControlWS.asmx on each node of the relationship.  In load balanced environments ensure that you test each node explicitly.
    1. Check that this is the same as the WSPath in the AppSettings
    2. Check that this is the same in the sitedb.config file in the Ektron Windows Service folder
  4. Ensure that each node is accessible on port 8732 from the next nodes in the chain
    1. From the command line enter ‘telnet servername 8732’ (in Server 2008/Windows 7 you will need to install the telnet client)
    2. If a connection is successful, you fill see a blank screen press ctrl+c to exit and restart the service you just connected to
  5. Ensure that the SearchConfigUI tool can properly iterate through all of the IIS websites (it will group results by unique connectionstrings).
    1. If the SearchConfigUI fails on an IIS 7 box ensure that IIS 6 Management Tools and Metabase compatability have been installed
  6. Ensure that all servers are using the same version of Ektron
    1. Compare the filesize (and versions) of the following files within the Windows Service folder
      • Ektron.ASM.EktronServices30.exe
      • Ektron.ASM.AssetConfig.dll
      • Ektron.FileSync.Common.dll
      • Ektron.FileSync.Framework.dll
      • Ektron.Sync.Communication.dll
      • Ektron.Sync.SyncServices.dll
  7. Ensure that the website is built against the same version of ektron as installed on each of the nodes
    1. Check that the common libraries within the website bin folder and windows service folders match
  8. Check that the website bin directories are the same size on all nodes within the relationship
  9. Check the Windows Service Error Log for indications of a Transport Layer Error (log files are within the /logs folder)
    1. The following errors can indicate a DNS or Proxy issue:
      • The remote server returned an unexpected response: (407) Proxy Authentication Required.
      • The request failed with HTTP status 403: Forbidden
      • The HTTP request was forbidden with client authentication scheme 'Anonymous'
  10. Ensure all nodes of the eSync relationship got all counterpart certificates installed (using Security Configurator)?
    1. The Ektron Windows Service directory will contain all of the *_SyncClient.* files for each node and the *_SyncServer.* files for the local machine
    2. The website root direction should contain the local machines *_SyncClient.* files and the timestamps should match the equivalent certificates in the Windows Service directory
    3. Check that local machines certificates are referenced within the ektron.serviceModel section of the web.config
    4. The Ektron.ASM.EktronServices30.exe.config file within the Windows Service folder should contain an element called publicCertKeys the has a reference to a client certificate for each server in the eSync relationship.  These should be present on every machine in the relationship (they may be in a different order).
    5. The website on each machine should have an AppSetting setting called EncodedValue this should be identical to the value stored within the Service Config file for the same machine.  If not, copy from the Windows Service config into the AppSetting not vice-versa.

Common eSync Errors

(as seen on Synchronisation Progress screen)
Object Reference Not Set To An Instance of An Object
  1. Check that the client certificate is present in the root of the website
  2. Check that local machines certificates are referenced within the ektron.serviceModel section of the web.config
  3. Check the WSPath is correct and acccessble
  4. Ensure the AppPool is runnning under the Network Service identity
The communication object, System.ServiceModel.Channels.ServiceChannel, cannot be used for communication because it is in the Faulted state
  1. Restart all services
  2. Check that licenses are installed and valid
  3. Check that certificates are installed
  4. Check the WSPath is correct and acccessble
  5. Compare the EncodedValue setting in the web.config with the value stored in the Ektron Service config
SyncDatabaseFailed failed with message: Max index do not match. Local index:X, Remote index:Y
  1. Decide which server has the correct index (ie if one server has other eSync profiles that are working ok)
  2. On the server that has the wrong index open the ServerInfo.xml (within the x:\sync folder)
  3. Change all of the MaxId="X" values to the correct value
  4. Open the Ektron database for the website with the incorrect index and modify the 'share_index' field of the settings table to the correct value
  5. Restart all Ektron Windows Services within the relationship
Invalid method invocation. Remote position is NOT staged.
  1. Stop all Ektron Windows Services within the relationship
  2. Create a copy of the x:\sync folder (where x is the drive letter for your workarea website)
  3. Delete all of the contents of the x:\sync folder
  4. Restart all of the Ektron Windows Services within the relationship
  5. Rerun synchronisation to rebuild relationship
SyncAssetLibraryFailed failed with message: Object reference not set to an instance of an object.
  1. Ensure that the ‘StorageLocation’ attribute of ‘DocumentManagerData’ is set to a unique location for each environment (this is within the AssetManagement.config) (if hosted on the same server)
No CMS400.NET sites were found at this location (when configuring a new relationship)
  1. Ensure that the certificates have been correctly installed on each node of the relationship.
The Remote Position is NOT Staged (when performaning an intitial sync)
  1. If a previous relationship existed between the two servers, ensure that the records relating to them have been removed from the [dbo].[scheduler] database table.
  2. Remove all folders relating to the previous relationship from the x:\sync folder (and in all child folders) 
  3. Try running the sync from the target website
  4. If problem persists, recreate the target min database – a previous initial sync attempt has semi-populated the target database beyond economical repair.
eSync Status is ‘Running’ Even When There Is No Sync In Progress
  1. Ensure that all Ektron services related to the website (development machines, build servers, etc, etc) have exactly the same version.
  2. Manually set the status to ‘completed’ in the [dbo].[scheduler] table and restart the services.
  3. If the problem recurs using SQL Profiler to find which server is updating the status (using the additional  HostName column).  The problem  command starts with ‘exec cms_updateschedulerun
Service Not Listening on port 8732 and EktronL2 Event Log contains ‘Service initialized successfully’ and ‘Service stopped successfully’
  1. Ensure that the workarea can be opened without an error (cmslogin.aspx and /workarea/servercontrolws.asmx)
  2. Restart service
  3. Ensure ‘Service started successfully’ message is recorded
SyncDatabaseFailed failed with message: The message with Action 'http://tempuri.org/ISyncService/InitSession' cannot be processed at the receiver, due to a ContractFilter mismatch at the EndpointDispatcher
  1. Ensure that the same esync service version is installed on all nodes of the relationship
Failed to execute the command 'DeleteCommand' for table 'content_folder_tbl' (or similar)
  1. There’s been an irresolvable conflict, try reversing the conflict policy temporarily and resyncing after a successful sync, the original policy can be restored.
Initial Sync Fails with a Database Constraint Error
The initial sync fails with a constraint error similar to:
  • Violation of PRIMARY KEY constraint 'PK_history_meta_tbl'
  • The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "folder_taxonomy_tbl_fk2"
  • Violation of PRIMARY KEY constraint 'PK_content_meta_tbl'
  1. Remove the damaged sync profile (see below)
  2. Stop users from editing content whilst the initial sync is run
Initial Sync Fails with:  Could not find file ‘….\uploadedFiles\\metaconfig.doc'
  1. Copy ‘metaconfig.doc’ from /assets into the uploadedFiles and uploadedImages
  2. Run SearchConfigUI and rebuild catalogs
  3. Re-run the intitial sync (it should pick up where it left off!).  You shouldn’t need to remove the relationship and rebuild.
Out of Memory Exceptions
  1. Ensure that all servers have adequate memory
  2. Reduce the batch size of the Ektron Windows Service by adding batchsize=100 to the 'DatabaseRuntime’ element within DbSync.config in the Ektron Windows Service folder.
  3. Ensure that all servers in the relationship have the same batchsize configured.
  4. Restart Ektron Service
  5. Rerun sync

The system can not find the file specified

  1. Ensure that the three *_SyncClient.* files the the local machine
  2. Ensure that local machines certificates are referenced within the ektron.serviceModel section of the web.config
  3. If the certificates have been accidently deleted, you can copy them from the Ektron Windows Service folder into the root folder.

Removing a Damaged eSync Relationship

If an initial sync has failed it can leave a partially populated sync relationship that will need to be removed.  Each time this is run the Max Server index will be incremented.
The procedure to do this is:
  1. Stop Ektron Service
  2. Deleting the target database
  3. Remove the corresponding entries from 'scheduler' table in the sending database
  4. Remove entries from 'X:\sync\ServerInfo.xml' on both servers
  5. Remove any folders (for the target environment) in X:\sync folder
  6. Remote entries from sitedb.conf for connections on both servers
  7. Remove contents of 'assets' and 'privateAssets' on target server
  8. Reinstall min database (using Site Setup)
  9. Recreate indexies using SearchConfigUI on target server
  10. Configure and run initial sync

Monday 22 March 2010

Ektron and Web Application Projects (Part 1)

Like a lot of enterprise CMS production Ektron expects you to work in a particular way and provides lots of help for non-developers to quickly produce websites with minimal technical skills.  But what if you have more technical needs?

One of the downsides (in my opinion) of developing websites against Ektron, is the expectation that you will be integrating your code into the existing workarea Web Site Project. 

Now, there are pros and cons on Web Site over Web Application projects but the big disadvantage for me is the sheer size of the workarea brings Visual Studio to a crawl when your editing code.  This is (partly) because of the background compilation that happens in the background to support IntelliSense.

There’s also some Best Practise issues:

  • Code Standards:  By ‘inheriting’ all of the 3rd Party code into your code base you’ve also limited your ability to ensure that your code works properly.  Static Analysis rules.
  • Separation of Concerns: Does your front end code need live in the same project as the administration screen?
  • Clean Builds:  Unfortunately, the workarea doesn’t build cleanly – there are compilation warnings, these will show up on any build metric reports….not good.  Of course, you can fix the warnings but then you’ll need to maintain the fixes with every release.
  • Performance: You may only have a three template website, but with an integrated workarea IIS still has to load all of the work area dependencies this takes time…and memory.
  • Security: By integrating into the 3rd Party code you’ve increased the Surface Area for attackers.  If you’re public facing website isn’t properly secured there are several Google queries that you can be run by hackers to find your site (and even the deployed version). (You can stop this by properly configuring your robots.txt – it won’t make your website bullet proof, but it will be harder for hackers/bots to find you!  And change your default admin account!)

So to my mind, there are three ways of developing against Ektron:

  1. Work within the workarea website:  If your working on a small site, don’t currently apply any best practise processes in your build process (or you may have a monster development PC).  You’ll be able to leverage all of the built in Ektron functionality with minimal work.
  2. Build your Website in a WAP and deploy to workarea website:  Keeps your code tidy and separate.  Good for running static analysis tools, etc.  It’s faster to develop (less VS overhead) and you can still use all of the Ektron functionality.
  3. Build your website in a WAP and deploy to a separate location (no workarea):  Disregard the provided controls and use the provided Ektron API as a repository.  Takes more development effort but gives you total control over the performance/security of the website.

In my next few posts, I’ll be covering how to achieve solutions 2. and 3. and what compromises you’ll need to make along the way…

  • Part 2 describes the folder structure and project configuration that you’ll need

Saturday 20 March 2010

ASP.Net and Custom Error Pages, an SEO nightmare?

It’s a (conscientious) developer’s worst fear:

You’ve slaved long and hard to produce a top-notch, blistering fast website that fully shows off your coding prowess and skills, you unveil the website to critical acclaim and universal client approval (imagine the cheering crowds) but then out-of-hours the database server fails!  Yerrk!

All the developer’s are out celebrating a successful so no-one notices the log file growing bigger and bigger, screaming to be heard…..and then the client notices and it really hits the fan!

After a frantic few hours the DB hardware’s restored and the website returns to it’s former glory….crisis over.

Or is it?

You’re a good web developer, of course you are, you’ve enabled the custom error pages in ASP.Net (you’ve probably event set the servers deployment property to retail to make doubly sure).  Every visitor to the site during your downtime would have seen a nice and friendly message informing them that there’s a problem with the site and it will be up again shortly.

Maybe, you’ve even reduced the potential impact by adding some marketing blurb about how great your products are, nice meaning full copy to make your visitors want to return to the site when it’s back when the sites back up.  In fact, just the sort of copy that search engines love – D’oh!  If your site has had issues whilst being indexed, that error message could very well come back to haunt you!

Now in fairness, it’s unlikely that your error page will be ranked as the #1 authority for a particular subject. Search engines have gotten pretty good at spotting these gaffs, but it won’t be helping your page rankings any either.

Here’s why, when your ASP.Net application throws an unhandled exception it naturally wants to return a 500 status code (Internal Server Error) and if you don’t have custom errors switched on, that’s what you get which leads to the Yellow Screen of Death!  But with custom error handling switched on there’s a neat little HttpModule that detects the 500 status code (Internal Server Error) and turns this in a redirect (302 status code) to your nice custom error page.  This then renders successfully (they’ll be no errors with your error page after all!) and returns a 200 status (OK).

For a visitor (human or web crawler), this looks exactly the same as a normal redirect.

Clearly then what we need is a smarter custom error HttpModule to selectively redirect the visitor based on whether or not they’re a search engine.  In fact, one just like this:

   1: using System;
   2: using System.Web;
   3: using System.Net;
   4: using System.Collections.Generic;
   5: using System.Configuration;
   6: using System.Web.Configuration;
   7:  
   8:  
   9: namespace MartinOnDotNet.Website.Support
  10: {
  11:     /// <summary>
  12:     /// Handles errors in an SEO friendly manner
  13:     /// </summary>
  14:     public class SeoErrorLoggingModule : IHttpModule
  15:     {
  16:  
  17:         /// <summary>
  18:         /// Called when [error].
  19:         /// </summary>
  20:         /// <param name="sender">The sender.</param>
  21:         /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
  22:         protected virtual void OnError(object sender, EventArgs e)
  23:         {
  24:             HttpApplication application = (HttpApplication)sender;
  25:             HttpContext context = application.Context;
  26:             if (context != null && context.AllErrors != null)
  27:             {
  28:                 foreach (Exception ex in context.AllErrors)
  29:                 {
  30:                     ex.Data["RawUrl"] = context.Request.RawUrl;
  31:                     HttpException hex = ex as HttpException;
  32:                     if (hex != null && hex.GetHttpCode() == (int)HttpStatusCode.NotFound)
  33:                     {
  34:                         Logging.Logger.LogWarning(string.Format(System.Globalization.CultureInfo.InvariantCulture, "Requested File Not Found {0} ({1})", context.Request.RawUrl, context.Request.Url));
  35:                     }
  36:                     else
  37:                     {
  38:                         Logging.Logger.Log(ex);
  39:                     }
  40:                    
  41:                 }
  42:             }
  43:             HttpException httpException = context.Error as HttpException;
  44:             context.Response.Clear();
  45:             if (httpException != null)
  46:                 context.Response.StatusCode = httpException.GetHttpCode();
  47:             else
  48:                 context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
  49:             if (context.IsCustomErrorEnabled
  50:                 && !context.Request.Browser.Crawler
  51:                 && !IsAnErrorPage(context.Request.RawUrl))
  52:             {
  53:                 context.ClearError();
  54:                 string path = GetPathForError(context, (HttpStatusCode)context.Response.StatusCode);
  55:                 if (!string.IsNullOrEmpty(path))
  56:                 {
  57:                     context.Response.Redirect(path, true);
  58:                 }
  59:             }
  60:         }
  61:  
  62:         /// <summary>
  63:         /// Gets the path for error.
  64:         /// </summary>
  65:         /// <param name="current">The current.</param>
  66:         /// <param name="status">The status.</param>
  67:         /// <returns></returns>
  68:         protected virtual string GetPathForError(HttpContext current, HttpStatusCode status)
  69:         {
  70:             CustomErrorsSection customErrors = WebConfigurationManager.GetSection("system.web/customErrors") as CustomErrorsSection;
  71:             foreach (CustomError ce in customErrors.Errors)
  72:             {
  73:                 if (ce.StatusCode == (int)status) return ce.Redirect;
  74:             }
  75:             return customErrors.DefaultRedirect;
  76:         }
  77:  
  78:         /// <summary>
  79:         /// Determines whether the given path (RawUrl) is an error page itself
  80:         /// </summary>
  81:         /// <param name="path">The path.</param>
  82:         /// <returns>
  83:         ///     <c>true</c> if [is an error page] [the specified path]; otherwise, <c>false</c>.
  84:         /// </returns>
  85:         protected virtual bool IsAnErrorPage(string path)
  86:         {
  87:             if (ErrorPages != null)
  88:             {
  89:                 foreach (string s in ErrorPages)
  90:                 {
  91:                     if (path.IndexOf(s, StringComparison.OrdinalIgnoreCase) > -1) return true;
  92:                 }
  93:             }
  94:             return false;
  95:         }
  96:  
  97:         /// <summary>
  98:         /// Gets the error pages.
  99:         /// </summary>
 100:         /// <value>The error pages.</value>
 101:         protected virtual IEnumerable<string> ErrorPages
 102:         {
 103:             get
 104:             {
 105:                 CustomErrorsSection customErrors = WebConfigurationManager.GetSection("system.web/customErrors") as CustomErrorsSection;
 106:                 foreach (CustomError ce in customErrors.Errors)
 107:                 {
 108:                     yield return ce.Redirect;
 109:                 }
 110:                 yield return customErrors.DefaultRedirect;
 111:             }
 112:         }
 113:  
 114:        /// <summary>
 115:        /// Disposes of the resources (other than memory) used by the module that implements <see cref="T:System.Web.IHttpModule"/>.
 116:        /// </summary>
 117:        public void Dispose()
 118:        {
 119:            //clean-up code here.
 120:        }
 121:  
 122:        /// <summary>
 123:        /// Initializes a module and prepares it to handle requests.
 124:        /// </summary>
 125:        /// <param name="context">An <see cref="T:System.Web.HttpApplication"/> that provides access to the methods, properties, and events common to all application objects within an ASP.NET application</param>
 126:        public void Init(HttpApplication context)
 127:        {
 128:            // Below is an example of how you can handle LogRequest event and provide 
 129:            // custom logging implementation for it
 130:            context.Error += new EventHandler(OnError);
 131:        }
 132:  
 133:  
 134:     }
 135: }

You’ll notice that this module also handles logging the error and differentiates between real exceptions (500) and file not found (404)  allowing a custom page to be displayed for either. 

To use the code simply register the module in the system.web/httpModules (for IIS 6) of System.Webserver/Modules (for IIS 7) section of your web.config and you’re good to go.  For best affect, it will probably be worthwhile putting an up-to-day .browsers file in your App_Browsers directory as well.