5

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;
    }
4

3 回答 3

5

最终,问题是由于没有区分传统 ASP.NET 应用程序和 ASP.NET MVC 应用程序提供的不同类型的上下文引起的。通过提供检查以确定我正在处理的上下文类型,我能够做出相应的响应。

我为 HttpTransfer 和 MvcTransfer 添加了单独的方法,允许我重定向到错误页面,特别是在需要时。我还更改了逻辑,以便我可以轻松地在我的本地和开发机器上获取我的 YSOD,而无需处理程序吞下异常。

除了用于将异常记录到数据库的代码(由 TODO 注释表示)之外,我们使用的最终代码是:

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
}
于 2009-10-23T16:49:41.190 回答
0

如果我理解正确,您只想处理导致 404 的操作的错误?

您可以检查请求的路由是否为空或停止路由 - 这本质上是 url 路由处理程序如何确定请求是否应继续进入 mvc 管道。

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;
}

顺便说一句,由于 404 导致的重定向会导致用户体验不太理想,并且是 ASP.NET 的后遗症(您无法将视图处理与请求处理分开)。管理 404 的正确方法是向浏览器返回 404 状态代码并显示您的自定义错误页面,而不是重定向(这会导致向浏览器发送 302 状态代码)。

于 2009-10-10T00:03:12.900 回答
0

为什么不在 global.asax 中捕获 404?

protected void Application_Error(object sender, EventArgs args) {

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

    }
}
于 2009-09-20T17:48:28.567 回答