views:

539

answers:

3

I have an HttpModule that I have put together from cobbling a couple of different sources online together into something that (mostly) works with both traditional ASP.NET applications, as well as ASP.NET MVC applications. The largest part of this comes from the kigg project on CodePlex. My problem is in dealing with 404 errors due to a missing image. In the following code, I have had to explicitly look for an image being requested through the AcceptedTypes collection in the HttpContext's Request object. If I don't put in this check, even a missing image is causing a redirect to the 404 page defined in my section in the Web.config.

The problem with this approach is that (beyond the fact it smells) is that this is just for images. I would basically have to do this with every single content type imaginable that I do not want this redirect behavior to happen on.

Looking at the code below, can someone recommend some sort of refactoring that could allow for it to be more lenient with non-page requests? I would still want them in the IIS logs (so I would probably have to remove the ClearError() call), but I do not think that a broken image should impact the user experience to the point of redirecting them to the error page.

The code follows:

/// <summary>
/// Provides a standardized mechanism for handling exceptions within a web application.
/// </summary>
public class ErrorHandlerModule : IHttpModule
{
    #region Public Methods

    /// <summary>
    /// Disposes of the resources (other than memory) used by the module that implements 
    /// <see cref="T:System.Web.IHttpModule"/>.
    /// </summary>
    public void Dispose()
    {
    }

    /// <summary>
    /// Initializes a module and prepares it to handle requests.
    /// </summary>
    /// <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>
    public void Init(HttpApplication context)
    {
        context.Error += this.OnError;
    }

    #endregion

    /// <summary>
    /// Called when an error occurs within the application.
    /// </summary>
    /// <param name="source">The source.</param>
    /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
    private void OnError(object source, EventArgs e)
    {
        var httpContext = HttpContext.Current;

        var imageRequestTypes =
            httpContext.Request.AcceptTypes.Where(a => a.StartsWith("image/")).Select(a => a.Count());

        if (imageRequestTypes.Count() > 0)
        {
            httpContext.ClearError();
            return;
        }

        var lastException = HttpContext.Current.Server.GetLastError().GetBaseException();
        var httpException = lastException as HttpException;
        var statusCode = (int)HttpStatusCode.InternalServerError;

        if (httpException != null)
        {
            statusCode = httpException.GetHttpCode();
            if ((statusCode != (int)HttpStatusCode.NotFound) && (statusCode != (int)HttpStatusCode.ServiceUnavailable))
            {
                // TODO: Log exception from here.
            }
        }

        var redirectUrl = string.Empty;

        if (httpContext.IsCustomErrorEnabled)
        {
            var errorsSection = WebConfigurationManager.GetSection("system.web/customErrors") as CustomErrorsSection;
            if (errorsSection != null)
            {
                redirectUrl = errorsSection.DefaultRedirect;

                if (httpException != null && errorsSection.Errors.Count > 0)
                {
                    var item = errorsSection.Errors[statusCode.ToString()];

                    if (item != null)
                    {
                        redirectUrl = item.Redirect;
                    }
                }
            }
        }

        httpContext.Response.Clear();
        httpContext.Response.StatusCode = statusCode;
        httpContext.Response.TrySkipIisCustomErrors = true;
        httpContext.ClearError();

        if (!string.IsNullOrEmpty(redirectUrl))
        {
            var mvcHandler = httpContext.CurrentHandler as MvcHandler;
            if (mvcHandler == null)
            {
                httpContext.Server.Transfer(redirectUrl);                    
            }
            else
            {
                var uriBuilder = new UriBuilder(
                    httpContext.Request.Url.Scheme, 
                    httpContext.Request.Url.Host, 
                    httpContext.Request.Url.Port, 
                    httpContext.Request.ApplicationPath);

                uriBuilder.Path += redirectUrl;

                string path = httpContext.Server.UrlDecode(uriBuilder.Uri.PathAndQuery);
                HttpContext.Current.RewritePath(path, false);
                IHttpHandler httpHandler = new MvcHttpHandler();

                httpHandler.ProcessRequest(HttpContext.Current);
            }
        }
    }
}

Any feedback would be appreciated. The app that I am currently doing this with is an ASP.NET MVC application, but like I mentioned it is written to work with an MVC handler, but only when the CurrentHandler is of that type.

Edit: I forgot to mention the "hack" in this case would be the following lines in OnError():

        var imageRequestTypes =
        httpContext.Request.AcceptTypes.Where(a => a.StartsWith("image/")).Select(a => a.Count());

    if (imageRequestTypes.Count() > 0)
    {
        httpContext.ClearError();
        return;
    }
A: 

why don't you catch the 404 in your global.asax?

protected void Application_Error(object sender, EventArgs args) {

    var ex = Server.GetLastError() as HttpException;
    if (ex != null && ex.ErrorCode == -2147467259) {

    }
}
Anthony Johnston
The point of the module is a "write once" solution so that I wouldn't have to put ad-hoc code in the Global.asax. I think I might have a modification to the HttpModule that is working. I'm going to simmer on it for a bit and run some tests to mak
joseph.ferris
er... Submitted too soon. I'm going to simmer on it for a bit and run some tests to sure it is up to snuff and post it back here tomorrow if I don't see any problems.
joseph.ferris
A: 

If I understand correctly, you only want to handle errors for actions that result in a 404?

You can check if the route for the request is null or stop routed - this is essentially how the url routing handler works to decide if the request should continue into the mvc pipeline.

var iHttpContext = new HttpContextWrapper( httpContext );
var routeData = RouteTable.Routes.GetRouteData( iHttpContext );
if( routeData == null || routeData.RouteHandler is StopRoute )
{
  // This is a route that would not normally be handled by the MVC pipeline
  httpContext.ClearError();
  return;
}

As an aside, redirecting due to a 404 causes a less than ideal user experience and is a hangover from ASP.NET ( where you couldn't separate the view processing from the request processing ). The correct way to manage a 404 is return a 404 status code to the browser and display your custom error page rather than redirect ( which results in a 302 status code sent to the browser ).

Neal
A: 

Ultimately, the problem was being caused by not distinguishing between the different types of Context provided by a traditional ASP.NET Application and an ASP.NET MVC Application. By providing a check to determine the type of context I was dealing with, I was able to respond accordingly.

I have added separate methods for an HttpTransfer and MvcTransfer that allow for me to redirect to the error page, specifically when needed. I also changed the logic around so that I could easily get my YSOD on my local and development machines without the handler swallowing the exception.

With the exception of the code used to log the exception to the database (denoted by a TODO comment), the final code that we are using is:

using System;
using System.Net;
using System.Security.Principal;
using System.Web;
using System.Web.Configuration;
using System.Web.Mvc;

using Diagnostics;

/// <summary>
/// Provides a standardized mechanism for handling exceptions within a web application.
/// </summary>
public sealed class ErrorHandlerModule : IHttpModule
{
    #region Public Methods

    /// <summary>
    /// Disposes of the resources (other than memory) used by the module that implements 
    /// <see cref="T:System.Web.IHttpModule"/>.
    /// </summary>
    public void Dispose()
    {
    }

    /// <summary>
    /// Initializes a module and prepares it to handle requests.
    /// </summary>
    /// <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>
    public void Init(HttpApplication context)
    {
        context.Error += OnError;
    }

    #endregion

    #region Private Static Methods

    /// <summary>
    /// Performs a Transfer for an MVC request.
    /// </summary>
    /// <param name="url">The URL to transfer to.</param>
    /// <param name="currentContext">The current context.</param>
    private static void HttpTransfer(string url, HttpContext currentContext)
    {
        currentContext.Server.TransferRequest(url);
    }

    /// <summary>
    /// Performs a Transfer for an MVC request.
    /// </summary>
    /// <param name="url">The URL to transfer to.</param>
    /// <param name="currentContext">The current context.</param>
    private static void MvcTransfer(string url, HttpContext currentContext)
    {
        var uriBuilder = new UriBuilder(
            currentContext.Request.Url.Scheme,
            currentContext.Request.Url.Host,
            currentContext.Request.Url.Port,
            currentContext.Request.ApplicationPath);

        uriBuilder.Path += url;

        string path = currentContext.Server.UrlDecode(uriBuilder.Uri.PathAndQuery);
        HttpContext.Current.RewritePath(path, false);
        IHttpHandler httpHandler = new MvcHttpHandler();

        httpHandler.ProcessRequest(HttpContext.Current);
    }

    #endregion

    #region Private Methods

    /// <summary>
    /// Called when an error occurs within the application.
    /// </summary>
    /// <param name="source">The source.</param>
    /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
    private static void OnError(object source, EventArgs e)
    {
        var httpContext = HttpContext.Current;
        var lastException = HttpContext.Current.Server.GetLastError().GetBaseException();
        var httpException = lastException as HttpException;
        var statusCode = (int)HttpStatusCode.InternalServerError;

        if (httpException != null)
        {
            if (httpException.Message == "File does not exist.")
            {
                httpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
                httpContext.ClearError();
                return;
            }

            statusCode = httpException.GetHttpCode();
        }

        if ((statusCode != (int)HttpStatusCode.NotFound) && (statusCode != (int)HttpStatusCode.ServiceUnavailable))
        {
            // TODO : Your error logging code here.
        }

        var redirectUrl = string.Empty;

        if (!httpContext.IsCustomErrorEnabled)
        {
            return;
        }

        var errorsSection = WebConfigurationManager.GetSection("system.web/customErrors") as CustomErrorsSection;
        if (errorsSection != null)
        {
            redirectUrl = errorsSection.DefaultRedirect;

            if (httpException != null && errorsSection.Errors.Count > 0)
            {
                var item = errorsSection.Errors[statusCode.ToString()];

                if (item != null)
                {
                    redirectUrl = item.Redirect;
                }
            }
        }

        httpContext.Response.Clear();
        httpContext.Response.StatusCode = statusCode;
        httpContext.Response.TrySkipIisCustomErrors = true;
        httpContext.ClearError();

        if (!string.IsNullOrEmpty(redirectUrl))
        {
            var mvcHandler = httpContext.CurrentHandler as MvcHandler;
            if (mvcHandler == null)
            {
                try
                {
                    HttpTransfer(redirectUrl, httpContext);
                }
                catch (InvalidOperationException)
                {
                    MvcTransfer(redirectUrl, httpContext);
                }
            }
            else
            {
                MvcTransfer(redirectUrl, httpContext);
            }
        }
    }

    #endregion
}
joseph.ferris