【问题标题】:HttpModule for Error Handling and Missing Images用于错误处理和丢失图像的 HttpModule
【发布时间】:2010-11-29 15:51:33
【问题描述】:

我有一个 HttpModule,我将几个不同的在线资源拼凑在一起,形成了一个(大部分)既适用于传统 ASP.NET 应用程序,也适用于 ASP.NET MVC 应用程序的东西。其中最大的一部分来自 CodePlex 上的 kigg 项目。我的问题是由于缺少图像而处理 404 错误。在下面的代码中,我必须显式查找通过 HttpContext 的 Request 对象中的 AcceptedTypes 集合请求的图像。如果我不进行此检查,即使缺少图像也会导致重定向到我在 Web.config 部分中定义的 404 页面。

这种方法的问题在于(除了它闻起来的事实之外)这仅适用于图像。我基本上必须对我不希望这种重定向行为发生的每一种可以想象的内容类型执行此操作。

看看下面的代码,有人可以推荐某种重构方式,让它对非页面请求更加宽容吗?我仍然希望它们出现在 IIS 日志中(因此我可能不得不删除 ClearError() 调用),但我认为损坏的图像不会影响用户体验,以至于将它们重定向到错误页面。

代码如下:

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

任何反馈都将不胜感激。我目前正在使用的应用程序是一个 ASP.NET MVC 应用程序,但就像我提到的那样,它是为使用 MVC 处理程序而编写的,但仅当 CurrentHandler 属于该类型时。

编辑:我忘了提到在这种情况下的“hack”将是 OnError() 中的以下几行:

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

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

【问题讨论】:

  • 您是否考虑过使用现有的错误日志库之一,而不是构建自己的错误日志记录模块,例如 ELMAH (code.google.com/p/elmah) 或 ASP.NET 健康监控 (msdn.microsoft.com/en-us/library/ms998306.aspx) ? ELMAH 具有丰富的错误过滤 API,如果需要,您可以在 Web.config 中以声明方式或通过代码指定。
  • Scott,我确实考虑过并且过去使用过 ELMAH。这更像是一次编码练习。

标签: asp.net asp.net-mvc error-handling http-status-code-404 httpmodule


【解决方案1】:

最终,问题是由于没有区分传统 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
}

【讨论】:

    【解决方案2】:

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

    protected void Application_Error(object sender, EventArgs args) {
    
        var ex = Server.GetLastError() as HttpException;
        if (ex != null && ex.ErrorCode == -2147467259) {
    
        }
    }
    

    【讨论】:

    • 模块的重点是“一次编写”解决方案,这样我就不必将临时代码放入 Global.asax。我想我可能会对正在运行的 HttpModule 进行修改。我将在它上面慢炖一会儿,然后运行一些测试来制作
    • er... 提交得太早了。我将在上面慢煮一会儿,然后进行一些测试,以确保它符合要求,如果我没有发现任何问题,明天再将其发布回这里。
    • 在处理图像或其他静态内容的 404 时不会参考 Global.asax。
    【解决方案3】:

    如果我理解正确,您只想处理导致 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 状态代码)。

    【讨论】:

      猜你喜欢
      • 2021-01-18
      • 1970-01-01
      • 2012-01-11
      • 2018-10-18
      • 2017-01-19
      • 2023-03-15
      • 1970-01-01
      • 1970-01-01
      • 2016-07-20
      相关资源
      最近更新 更多