本主题概述 ASP.NET Web API 2 中用于 ASP.NET 4.x 的全局错误处理。 目前,Web API 无法全局记录或处理错误。 某些未经处理的异常可以通过 异常筛选器进行处理,但存在许多异常筛选器无法处理的情况。 例如:
- 从控制器构造函数引发的异常。
- 从消息处理程序引发的异常。
- 在路由过程中引发的异常。
- 在响应内容序列化期间引发的异常。
我们希望提供一种简单、一致的方法来记录和处理这些异常(尽可能)。
处理异常有两种主要情况,一种是我们能够发送错误响应,另一种是我们只能记录异常。 后一种情况的一个示例是在流式传输响应内容的过程中引发异常时,此时重新发送响应消息为时已晚,因为状态代码、标头和部分内容已经通过了网络,因此我们只能中止连接。 即使无法处理异常来生成新的响应消息,我们仍支持记录异常。 如果可以检测错误,可以返回相应的错误响应,如下所示:
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)来记录和处理未经处理的异常。 这些服务非常相似,主要有两种差异:
- 我们支持注册多个异常记录器,但仅注册单个异常处理程序。
- 异常记录器始终被调用,即使即将中止连接。 仅当仍能够选择要发送的响应消息时,才会调用异常处理程序。
这两个服务都提供对异常上下文的访问权限,其中包含检测到异常的点的相关信息,尤其是 HttpRequestMessage、 HttpRequestContext、引发的异常和异常源(详细信息如下)。
设计原理
- 没有重大更改 由于此功能是在次要版本中添加的,因此影响解决方案的一个重要约束是类型协定或行为没有重大更改。 此约束使得我们无法在现有的 catch 块中完成把异常转换为500响应的清理工作。 对于下一个主要版本,我们可能会考虑进行这种额外的清理。
- 保持与 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 块。 (这些基类的简化版本位于以下附录中。)IExceptionLogger和IExceptionHandler都通过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; }
}
当框架调用异常记录器或异常处理程序时,它将始终提供一个 Exception 和一个 Request。 除单元测试外,它还将始终提供一个 RequestContext。 它很少提供一个 ControllerContext 和 ActionContext (仅当从 catch 块调用异常筛选器时)。 它很少提供( Response仅在某些 IIS 情况下,在尝试编写响应时)。 请注意,由于其中一些属性可能是 null,因此使用者需要在访问异常类的成员之前检查 null。
CatchBlock 是一个字符串,指示哪个 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 属性设置为操作结果(例如 ExceptionResult、 InternalServerErrorResult、 StatusCodeResult 或自定义结果)来指示它已处理异常。
Result如果该属性为 null,则异常未经处理,并且将重新引发原始异常。
对于调用堆栈顶部的异常,我们采取了额外的步骤来确保响应适用于 API 调用方。 如果异常传播到主机,调用方将看到“黄色死屏”或其他某些主机提供的响应,这些响应通常是 HTML 格式,并不适合作为 API 错误响应。 在这些情况下,Result 初始值为非 null,仅当自定义异常处理程序显式将其设置回 null(未经处理的)时,异常才会扩散到主机。 将Result设置为null在此类情况下对两种情形都非常有用:
- OWIN 托管的 Web API,其中包含在 Web API 之前或之外注册的自定义异常处理中间件。
- 通过浏览器进行本地调试,其中黄色死机屏幕实际上是对一个未经处理的异常的有用响应。
对于异常记录器和异常处理程序,如果记录器或处理程序本身引发异常,我们不会执行任何操作来恢复。 (除了让异常传播之外,如果你采用更好的方法,请在此页面底部留下反馈。异常记录器和处理程序的协定是,它们不应让异常传播到其调用方;否则,异常只会传播,通常一直传播到主机,从而导致 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 配置代码放入类中的WebApiConfigApp_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;
}
}