ASP.NET Web API 2 中的全局错误处理

作者 :David MatsonRick Anderson

本主题概述了 ASP.NET 4.x ASP.NET Web API 2 中的全局错误处理。 目前,Web API 中没有简单的方法来记录或处理全局错误。 某些未经处理的异常可以通过 异常筛选器进行处理,但有一些异常筛选器无法处理的情况。 例如:

  1. 从控制器构造函数引发的异常。
  2. 从消息处理程序引发的异常。
  3. 在路由过程中引发的异常。
  4. 在响应内容序列化期间引发的异常。

我们希望提供一种简单、一致的方式来记录和处理 ((如果可能) 这些异常)。

处理异常主要有两种情况:我们可以发送错误响应,而我们可以做的只是记录异常。 后一种情况的示例是在流式处理响应内容中间引发异常时;在这种情况下,发送新的响应消息为时已晚,因为状态代码、标头和部分内容已经通过网络,因此我们只是中止连接。 即使无法处理异常来生成新的响应消息,我们仍支持记录异常。 在可以检测到错误的情况下,可以返回适当的错误响应,如下所示:

public IHttpActionResult GetProduct(int id)
{
    var product = products.FirstOrDefault((p) => p.Id == id);
    if (product == null)
    {
        return NotFound();
    }
    return Ok(product);
}

现有选项

除了 异常筛选器之外, 消息处理程序 目前还可用于观察所有 500 级响应,但对这些响应执行操作很困难,因为它们缺少有关原始错误的上下文。 消息处理程序在可以处理的情况方面也具有与异常筛选器相同的一些限制。 虽然 Web API 确实具有捕获错误情况的跟踪基础结构,但跟踪基础结构用于诊断目的,并且不适合在生产环境中运行。 全局异常处理和日志记录应该是可以在生产期间运行并插入现有监视解决方案的服务, (例如 ELMAH) 。

解决方案概述

我们提供两个新的用户可替换服务 IExceptionLogger 和 IExceptionHandler,用于记录和处理未经处理的异常。 这些服务非常相似,有两个main差异:

  1. 我们支持注册多个异常记录器,但只注册单个异常处理程序。
  2. 始终调用异常记录器,即使我们将中止连接。 仅当我们仍能够选择要发送的响应消息时,才会调用异常处理程序。

这两种服务都提供对异常上下文的访问,其中包含从检测到异常的位置获取的相关信息,特别是 HttpRequestMessageHttpRequestContext、引发的异常和异常源 () 下的详细信息。

设计原理

  1. 无中断性变更 由于此功能是在次要版本中添加的,因此影响解决方案的一个重要约束是,不存在对类型协定或行为的重大更改。 此约束排除了我们希望在将异常转换为 500 响应的现有 catch 块方面所做的一些清理。 对于后续的主要版本,我们可能会考虑进行这种额外的清理。
  2. 保持与 Web API 构造的一致性 Web API 的筛选器管道是处理交叉问题的好方法,可以灵活地在特定于操作、特定于控制器或全局范围内应用逻辑。 筛选器(包括异常筛选器)始终具有操作和控制器上下文,即使在全局范围内注册也是如此。 该协定对筛选器有意义,但这意味着异常筛选器(即使是全局范围的筛选器)不适合某些异常处理情况,例如消息处理程序中的异常,其中不存在任何操作或控制器上下文。 如果想要使用筛选器提供的灵活范围来处理异常,我们仍然需要异常筛选器。 但是,如果需要处理控制器上下文外部的异常,还需要一个单独的构造,用于完全全局错误处理 (没有控制器上下文和操作上下文约束) 。

何时使用

  • 异常记录器是查看 Web API 捕获的所有未经处理的异常的解决方案。
  • 异常处理程序是用于自定义对 Web API 捕获的未经处理的异常的所有可能响应的解决方案。
  • 异常筛选器是处理与特定操作或控制器相关的子集未经处理的异常的最简单解决方案。

服务详细信息

异常记录器和处理程序服务接口是采用相应上下文的简单异步方法:

public interface IExceptionLogger
{
   Task LogAsync(ExceptionLoggerContext context, 
                 CancellationToken cancellationToken);
}

public interface IExceptionHandler
{
   Task HandleAsync(ExceptionHandlerContext context, 
                    CancellationToken cancellationToken);
}

我们还为这两个接口提供基类。 (同步或异步) 方法替代核心,只需在建议的时间进行记录或处理即可。 对于日志记录, ExceptionLogger 基类将确保仅针对每个异常 (调用一次核心日志记录方法,即使它稍后会向调用堆栈进一步传播并) 捕获。 基 ExceptionHandler 类将仅针对调用堆栈顶部的异常调用核心处理方法,而忽略旧的嵌套 catch 块。 (这些基类的简化版本位于下面的附录中。) 通过 IExceptionLoggerIExceptionHandler 接收有关异常 ExceptionContext的信息。

public class ExceptionContext
{
   public Exception Exception { get; set; }

   public HttpRequestMessage Request { get; set; }

   public HttpRequestContext RequestContext { get; set; }

   public HttpControllerContext ControllerContext { get; set; }

   public HttpActionContext ActionContext { get; set; }

   public HttpResponseMessage Response { get; set; }

   public string CatchBlock { get; set; }

   public bool IsTopLevelCatchBlock { get; set; }
}

当框架调用异常记录器或异常处理程序时,它将始终提供 ExceptionRequest。 除单元测试外,它还将始终提供 RequestContext。 仅当从异常筛选器) 的 catch 块调用时,它很少提供 ControllerContextActionContext (。 仅在尝试编写响应) 时,它很少在某些 IIS 情况下提供 Response (。 请注意,由于其中某些属性可能null由使用者在访问异常类的成员之前检查nullCatchBlock 是一个字符串,指示哪个 catch 块看到异常。 catch 块字符串如下所示:

  • HttpServer (SendAsync 方法)

  • HttpControllerDispatcher (SendAsync 方法)

  • HttpBatchHandler (SendAsync 方法)

  • IExceptionFilter (ApiController 在 ExecuteAsync 中处理异常筛选器管道)

  • OWIN 主机:

    • HttpMessageHandlerAdapter.BufferResponseContentAsync (,用于缓冲输出)
    • HttpMessageHandlerAdapter.CopyResponseContentAsync (,用于流式处理输出)
  • Web 主机:

    • HttpControllerHandler.WriteBufferedResponseContentAsync (,用于缓冲输出)
    • HttpControllerHandler.WriteStreamedResponseContentAsync (,用于流式处理输出)
    • HttpControllerHandler.WriteErrorResponseContentAsync (,用于在缓冲输出模式下的错误恢复失败)

catch 块字符串的列表也通过静态只读属性提供。 (核心 catch 块字符串位于静态 ExceptionCatchBlocks 上;对于 OWIN 和 Web 主机) ,其余部分分别显示在一个静态类上。IsTopLevelCatchBlock 有助于遵循仅在调用堆栈顶部处理异常的建议模式。 异常处理程序可以让异常传播,直到主机即将看到异常,而不是在嵌套 catch 块发生时将异常转换为 500 个响应。

除了 , ExceptionContext记录器还会通过完整的 ExceptionLoggerContext获取一条信息:

public class ExceptionLoggerContext
{
   public ExceptionContext ExceptionContext { get; set; }
   public bool CanBeHandled { get; set; }
}

第二个属性 CanBeHandled允许记录器标识无法处理的异常。 当连接即将中止且无法发送任何新的响应消息时,将调用记录器,但 不会 调用处理程序,记录器可以从此属性中识别这种情况。

除了 , ExceptionContext处理程序还获取了一个属性,它可以在 完全 ExceptionHandlerContext 上设置来处理异常:

public class ExceptionHandlerContext
{
   public ExceptionContext ExceptionContext { get; set; }
   public IHttpActionResult Result { get; set; }
}

异常处理程序通过将 属性设置为 Result 操作结果 ((例如 ExceptionResultInternalServerErrorResultStatusCodeResult 或自定义结果) )来指示它已处理异常。 Result如果 属性为 null,则异常未处理,原始异常将重新引发。

对于调用堆栈顶部的异常,我们采取了额外的步骤来确保响应适用于 API 调用方。 如果异常传播到主机,调用方将看到死亡的黄屏或其他主机提供的响应,这通常是 HTML 的,通常不是适当的 API 错误响应。 在这些情况下,Result 将启动非 null,并且仅当自定义异常处理程序将其显式设置为 null (未经处理的) 才会将异常传播到主机。 在这种情况下,将 设置为 Resultnull 可用于两种情况:

  1. OWIN 托管的 Web API,其中包含在 Web API 之前/外部注册的自定义异常处理中间件。
  2. 通过浏览器进行本地调试,其中黄屏死机实际上是对未经处理的异常的有用响应。

对于异常记录器和异常处理程序,如果记录器或处理程序本身引发异常,我们不会执行任何恢复操作。 (除了允许异常传播之外,如果你有更好的方法,请在此页面底部留下反馈。) 异常记录器和处理程序的协定是,它们不应让异常传播到其调用方;否则,异常只会传播,通常一直传播到主机,从而导致 HTML 错误 (ASP。NET 的黄屏) 发送回客户端 (这通常不是需要 JSON 或 XML) 的 API 调用方的首选选项。

示例

跟踪异常记录器

下面的异常记录器将异常数据发送到配置的跟踪源 (包括 Visual Studio) 中的“调试输出”窗口。

class TraceExceptionLogger : ExceptionLogger
{
    public override void LogCore(ExceptionLoggerContext context)
    {
        Trace.TraceError(context.ExceptionContext.Exception.ToString());
    }
}

自定义错误消息异常处理程序

下面的异常处理程序会生成对客户端的自定义错误响应,包括用于联系支持人员的电子邮件地址。

class OopsExceptionHandler : ExceptionHandler
{
    public override void HandleCore(ExceptionHandlerContext context)
    {
        context.Result = new TextPlainErrorResult
        {
            Request = context.ExceptionContext.Request,
            Content = "Oops! Sorry! Something went wrong." +
                      "Please contact support@contoso.com so we can try to fix it."
        };
    }

    private class TextPlainErrorResult : IHttpActionResult
    {
        public HttpRequestMessage Request { get; set; }

        public string Content { get; set; }

        public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
        {
            HttpResponseMessage response = 
                             new HttpResponseMessage(HttpStatusCode.InternalServerError);
            response.Content = new StringContent(Content);
            response.RequestMessage = Request;
            return Task.FromResult(response);
        }
    }
}

注册异常筛选器

如果使用“ASP.NET MVC 4 Web 应用程序”项目模板来创建项目,请将 Web API 配置代码 WebApiConfig 放在 类中的 App_Start 文件夹中:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new ProductStore.NotImplExceptionFilterAttribute());

        // Other configuration code...
    }
}

附录:基类详细信息

public class ExceptionLogger : IExceptionLogger
{
    public virtual Task LogAsync(ExceptionLoggerContext context, 
                                 CancellationToken cancellationToken)
    {
        if (!ShouldLog(context))
        {
            return Task.FromResult(0);
        }

        return LogAsyncCore(context, cancellationToken);
    }

    public virtual Task LogAsyncCore(ExceptionLoggerContext context, 
                                     CancellationToken cancellationToken)
    {
        LogCore(context);
        return Task.FromResult(0);
    }

    public virtual void LogCore(ExceptionLoggerContext context)
    {
    }

    public virtual bool ShouldLog(ExceptionLoggerContext context)
    {
        IDictionary exceptionData = context.ExceptionContext.Exception.Data;

        if (!exceptionData.Contains("MS_LoggedBy"))
        {
            exceptionData.Add("MS_LoggedBy", new List<object>());
        }

        ICollection<object> loggedBy = ((ICollection<object>)exceptionData[LoggedByKey]);

        if (!loggedBy.Contains(this))
        {
            loggedBy.Add(this);
            return true;
        }
        else
        {
            return false;
        }
    }
}

public class ExceptionHandler : IExceptionHandler
{
    public virtual Task HandleAsync(ExceptionHandlerContext context, 
                                    CancellationToken cancellationToken)
    {
        if (!ShouldHandle(context))
        {
            return Task.FromResult(0);
        }

        return HandleAsyncCore(context, cancellationToken);
    }

    public virtual Task HandleAsyncCore(ExceptionHandlerContext context, 
                                       CancellationToken cancellationToken)
    {
        HandleCore(context);
        return Task.FromResult(0);
    }

    public virtual void HandleCore(ExceptionHandlerContext context)
    {
    }

    public virtual bool ShouldHandle(ExceptionHandlerContext context)
    {
        return context.ExceptionContext.IsOutermostCatchBlock;
    }
}