Andornot Blog

Friday, October 02, 2009 3:21 PM

Handling 404 errors with ASP.NET

by Ted Jardine

As mentioned at the end of my previous post on handling errors with ASP.NET, handling "404 Not Found" errors are particularly problematic (if you haven't read it yet, please do so). And looking around, the vast majority of information out there on it is not complete, misinformed, or flat-out wrong (but I greatly appreciate all efforts!). And I would argue that this is because ASP.NET implementation of 404 error handling is flat-out-wrong. So with my super hero cape on, here I come to wobbly save the day!

The typical ASP.NET way to handle 404 errors is to put something like the following in your Web.config:

<customErrors mode="On" defaultRedirect="~/error.aspx">
    <error statusCode="404" redirect="~/page-not-found.aspx" />
</customErrors>

Make a page-not-found.aspx page and voila! Ya got 'er dun! If you're a little more on the ball, you'll realise that while this configuration works for end users (gives them a pretty page to look at hopefully clearly explaining that you can't find what they're looking for), it's bad for SEO (search engine optimization) because it sends back a 302 temporary redirect to your 404 page which in turn sends back a "200 OK" message. In other words, "Yeehah! No problems here as you've found what you're looking for! Index away!"

So, bright developer that you are, you add in some applicable status code into your 404 page thinking that should take care of it:

protected override void OnLoad(System.EventArgs e)
{
    Response.TrySkipIisCustomErrors = true; // For IIS 7 Integrated Pipeline - see previous post
    Response.Status = "404 Not Found";
    Response.StatusCode = 404;
    base.OnLoad(e);
}

Well, fire up Fiddler and you'll discover that you're still getting a 302 temporary redirect to your 404 page. So you fire up your error handling code and for 404s, you Server.Transfer to your 404 page just like all your other error transfers take place! But no, bafflingly enough, even running through a debug session to ensure you're properly catching your 404, ASP.NET still insists on 302'ing your precious response (although at least now your 404 page is sending back the proper "404 Not Found" error status). So go out there and google everywhere and try every suggestion (just a sampling) and then breathe a prayer of thanksgiving for me and my super-duper super hero cape, because this is how you make your way to the 404 handling bliss (caveat: thoroughly tested only on IIS 7):

First, make something like the following in Application_Error in your global.asax (or better yet, a custom error module):

protected void Application_Error(Object sender, EventArgs e)
{
    Exception exception = Server.GetLastError();
    if (exception is HttpUnhandledException)
    {
        if (exception.InnerException == null)
        {
            Server.Transfer(ERROR_PAGE_LOCATION, false);
            return;
        }
        exception = exception.InnerException;
    }

    if (exception is HttpException)
    {
        if (((HttpException)exception).GetHttpCode() == 404)
        {

            // Log if wished.
            Server.ClearError();
            Server.Transfer(NOT_FOUND_PAGE_LOCATION, false);
            return;
        }
    }

    if (Context != null && Context.IsCustomErrorEnabled)
        Server.Transfer(ERROR_PAGE_LOCATION, false);
    else
        Log.Error("Unhandled Exception trapped in Global.asax", exception);
}

Second, put something like the following in your 404 page (or even better, add it to your above custom error module):

protected override void OnLoad(System.EventArgs e)
{
    Response.TrySkipIisCustomErrors = true; // For IIS 7 Integrated Pipeline - see previous post
    Response.Status = "404 Not Found";
    Response.StatusCode = 404;
    base.OnLoad(e);
}

And lastly, put whatever you want in your customErrors section in your Web.config for 404 - remove the 404 references if you want. The only time it would get used is if you keep the confusion level down by reading that section in your error module to find out what your 404 page is.

And now, you don't get any nasty 302 redirects. You get a blissfully pure and pretty 404.

Note: you can have everything in place as above, but if you forget to Server.ClearErrror(), it'll all be for naught as you'll still get 302 redirected (thanks to http://stackoverflow.com/questions/667053/best-way-to-implement-a-404-in-asp-net for finally helping me get this one nailed).

UPDATE: If for some reason you cannot change your code, a good option to pursue is flipping a switch in your applicationHost.config file that passes your response through without IIS hijacking it. This is the sledgehammer approach, but could be applicable for your situation. For example, in order to allow Umbraco's alternative status code responses to work, this is the only way to get them to avoid being hijacked by IIS (i.e. NotFoundHandlers, booting screens, etc.).

<location path="Site Description">
    <system.webServer>
        <httpErrors existingResponse="PassThrough" />
    </system.webServer>
</location>

Thanks to Fabian Heussser's comment on Rick Strahl's post that helped with this.

blog comments powered by Disqus

Month List