抛出 HttpResponseException 或返回 Request?

在回顾了一篇文章 ASP.NET Web API 中的异常处理之后,我对何时抛出异常与何时返回错误响应感到有些困惑。我还想知道是否有可能修改响应时,您的方法返回一个领域特定的模型,而不是 HttpResponseMessage..。

所以,这里回顾一下我的问题,后面跟着一些带有 case # s 的代码:

问题

有关个案1的问题

  1. 我是否应该总是使用 HttpResponseMessage而不是具体的域模型,以便消息可以自定义?
  2. 如果返回具体的域模型,是否可以自定义消息?

关于案例2,3,4的问题

  1. 我应该抛出异常还是返回错误响应?如果答案是“视情况而定”,你可以举例说明什么时候使用一种方法对另一种方法。
  2. 抛出 HttpResponseException和抛出 Request.CreateErrorResponse的区别是什么? 输出到客户端似乎相同..。
  3. 我是否应该总是使用 HttpError在错误中“包装”响应消息(无论是抛出异常还是返回错误响应) ?

案例样本

// CASE #1
public Customer Get(string id)
{
var customer = _customerService.GetById(id);
if (customer == null)
{
var notFoundResponse = new HttpResponseMessage(HttpStatusCode.NotFound);
throw new HttpResponseException(notFoundResponse);
}
//var response = Request.CreateResponse(HttpStatusCode.OK, customer);
//response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
return customer;
}


// CASE #2
public HttpResponseMessage Get(string id)
{
var customer = _customerService.GetById(id);
if (customer == null)
{
var notFoundResponse = new HttpResponseMessage(HttpStatusCode.NotFound);
throw new HttpResponseException(notFoundResponse);
}
var response = Request.CreateResponse(HttpStatusCode.OK, customer);
response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
return response;
}


// CASE #3
public HttpResponseMessage Get(string id)
{
var customer = _customerService.GetById(id);
if (customer == null)
{
var message = String.Format("customer with id: {0} was not found", id);
var errorResponse = Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
throw new HttpResponseException(errorResponse);
}
var response = Request.CreateResponse(HttpStatusCode.OK, customer);
response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
return response;
}


// CASE #4
public HttpResponseMessage Get(string id)
{
var customer = _customerService.GetById(id);
if (customer == null)
{
var message = String.Format("customer with id: {0} was not found", id);
var httpError = new HttpError(message);
return Request.CreateErrorResponse(HttpStatusCode.NotFound, httpError);
}
var response = Request.CreateResponse(HttpStatusCode.OK, customer);
response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
return response;
}

更新

为了帮助进一步演示案例 # 2、3、4,下面的代码片段突出显示了在找不到客户时“可能发生”的几个选项..。

if (customer == null)
{
// which of these 4 options is the best strategy for Web API?


// option 1 (throw)
var notFoundMessage = new HttpResponseMessage(HttpStatusCode.NotFound);
throw new HttpResponseException(notFoundMessage);


// option 2 (throw w/ HttpError)
var message = String.Format("Customer with id: {0} was not found", id);
var httpError = new HttpError(message);
var errorResponse = Request.CreateErrorResponse(HttpStatusCode.NotFound, httpError);
throw new HttpResponseException(errorResponse);


// option 3 (return)
var message = String.Format("Customer with id: {0} was not found", id);
return Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
// option 4 (return w/ HttpError)
var message = String.Format("Customer with id: {0} was not found", id);
var httpError = new HttpError(message);
return Request.CreateErrorResponse(HttpStatusCode.NotFound, httpError);
}
182612 次浏览

案例一

  1. 不一定,管道中还有其他地方可以修改响应(操作过滤器、消息处理程序)。
  2. 如上所述——但是如果操作返回一个域模型,那么您就不能修改响应 在里面操作。

个案 # 2-4

  1. 抛出 HttpResponseException 的主要原因是:
    • 如果您正在返回一个域模型,但是需要处理错误情况,
    • 通过将错误视为异常来简化控制器逻辑
  2. 这些应该是等价的; HttpResponseException 封装了一个 HttpResponseMessage,它作为 HTTP 响应返回。

    例如,案例 # 2可以重写为

    public HttpResponseMessage Get(string id)
    {
    HttpResponseMessage response;
    var customer = _customerService.GetById(id);
    if (customer == null)
    {
    response = new HttpResponseMessage(HttpStatusCode.NotFound);
    }
    else
    {
    response = Request.CreateResponse(HttpStatusCode.OK, customer);
    response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
    }
    return response;
    }
    

    但是如果您的控制器逻辑更加复杂,抛出异常可能会简化代码流。

  3. HttpError 为响应主体提供了一致的格式,可以序列化为 JSON/XML/etc,但不是必需的。例如,您可能不希望在响应中包含实体主体,或者您可能希望使用其他格式。

我所采用的方法是仅从 api 控制器操作中抛出异常,并注册一个异常过滤器,该过滤器处理异常并在操作执行上下文中设置适当的响应。

过滤器公开了一个流畅的接口,该接口提供了一种方法,可以在向全局配置注册过滤器之前为特定类型的异常注册处理程序。

此筛选器的使用支持集中式异常处理,而不是将异常分散到控制器操作中。然而,在某些情况下,我会在控制器操作中捕获异常,并在集中处理该特定异常没有意义时返回特定的响应。

过滤器注册示例:

GlobalConfiguration.Configuration.Filters.Add(
new UnhandledExceptionFilterAttribute()
.Register<KeyNotFoundException>(HttpStatusCode.NotFound)


.Register<SecurityException>(HttpStatusCode.Forbidden)


.Register<SqlException>(
(exception, request) =>
{
var sqlException = exception as SqlException;


if (sqlException.Number > 50000)
{
var response            = request.CreateResponse(HttpStatusCode.BadRequest);
response.ReasonPhrase   = sqlException.Message.Replace(Environment.NewLine, String.Empty);


return response;
}
else
{
return request.CreateResponse(HttpStatusCode.InternalServerError);
}
}
)
);

属性类:

using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Web.Http.Filters;


namespace Sample
{
/// <summary>
/// Represents the an attribute that provides a filter for unhandled exceptions.
/// </summary>
public class UnhandledExceptionFilterAttribute : ExceptionFilterAttribute
{
#region UnhandledExceptionFilterAttribute()
/// <summary>
/// Initializes a new instance of the <see cref="UnhandledExceptionFilterAttribute"/> class.
/// </summary>
public UnhandledExceptionFilterAttribute() : base()
{


}
#endregion


#region DefaultHandler
/// <summary>
/// Gets a delegate method that returns an <see cref="HttpResponseMessage"/>
/// that describes the supplied exception.
/// </summary>
/// <value>
/// A <see cref="Func{Exception, HttpRequestMessage, HttpResponseMessage}"/> delegate method that returns
/// an <see cref="HttpResponseMessage"/> that describes the supplied exception.
/// </value>
private static Func<Exception, HttpRequestMessage, HttpResponseMessage> DefaultHandler = (exception, request) =>
{
if(exception == null)
{
return null;
}


var response            = request.CreateResponse<string>(
HttpStatusCode.InternalServerError, GetContentOf(exception)
);
response.ReasonPhrase   = exception.Message.Replace(Environment.NewLine, String.Empty);


return response;
};
#endregion


#region GetContentOf
/// <summary>
/// Gets a delegate method that extracts information from the specified exception.
/// </summary>
/// <value>
/// A <see cref="Func{Exception, String}"/> delegate method that extracts information
/// from the specified exception.
/// </value>
private static Func<Exception, string> GetContentOf = (exception) =>
{
if (exception == null)
{
return String.Empty;
}


var result  = new StringBuilder();


result.AppendLine(exception.Message);
result.AppendLine();


Exception innerException = exception.InnerException;
while (innerException != null)
{
result.AppendLine(innerException.Message);
result.AppendLine();
innerException = innerException.InnerException;
}


#if DEBUG
result.AppendLine(exception.StackTrace);
#endif


return result.ToString();
};
#endregion


#region Handlers
/// <summary>
/// Gets the exception handlers registered with this filter.
/// </summary>
/// <value>
/// A <see cref="ConcurrentDictionary{Type, Tuple}"/> collection that contains
/// the exception handlers registered with this filter.
/// </value>
protected ConcurrentDictionary<Type, Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>> Handlers
{
get
{
return _filterHandlers;
}
}
private readonly ConcurrentDictionary<Type, Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>> _filterHandlers = new ConcurrentDictionary<Type, Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>>();
#endregion


#region OnException(HttpActionExecutedContext actionExecutedContext)
/// <summary>
/// Raises the exception event.
/// </summary>
/// <param name="actionExecutedContext">The context for the action.</param>
public override void OnException(HttpActionExecutedContext actionExecutedContext)
{
if(actionExecutedContext == null || actionExecutedContext.Exception == null)
{
return;
}


var type    = actionExecutedContext.Exception.GetType();


Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> registration = null;


if (this.Handlers.TryGetValue(type, out registration))
{
var statusCode  = registration.Item1;
var handler     = registration.Item2;


var response    = handler(
actionExecutedContext.Exception.GetBaseException(),
actionExecutedContext.Request
);


// Use registered status code if available
if (statusCode.HasValue)
{
response.StatusCode = statusCode.Value;
}


actionExecutedContext.Response  = response;
}
else
{
// If no exception handler registered for the exception type, fallback to default handler
actionExecutedContext.Response  = DefaultHandler(
actionExecutedContext.Exception.GetBaseException(), actionExecutedContext.Request
);
}
}
#endregion


#region Register<TException>(HttpStatusCode statusCode)
/// <summary>
/// Registers an exception handler that returns the specified status code for exceptions of type <typeparamref name="TException"/>.
/// </summary>
/// <typeparam name="TException">The type of exception to register a handler for.</typeparam>
/// <param name="statusCode">The HTTP status code to return for exceptions of type <typeparamref name="TException"/>.</param>
/// <returns>
/// This <see cref="UnhandledExceptionFilterAttribute"/> after the exception handler has been added.
/// </returns>
public UnhandledExceptionFilterAttribute Register<TException>(HttpStatusCode statusCode)
where TException : Exception
{


var type    = typeof(TException);
var item    = new Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>(
statusCode, DefaultHandler
);


if (!this.Handlers.TryAdd(type, item))
{
Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> oldItem = null;


if (this.Handlers.TryRemove(type, out oldItem))
{
this.Handlers.TryAdd(type, item);
}
}


return this;
}
#endregion


#region Register<TException>(Func<Exception, HttpRequestMessage, HttpResponseMessage> handler)
/// <summary>
/// Registers the specified exception <paramref name="handler"/> for exceptions of type <typeparamref name="TException"/>.
/// </summary>
/// <typeparam name="TException">The type of exception to register the <paramref name="handler"/> for.</typeparam>
/// <param name="handler">The exception handler responsible for exceptions of type <typeparamref name="TException"/>.</param>
/// <returns>
/// This <see cref="UnhandledExceptionFilterAttribute"/> after the exception <paramref name="handler"/>
/// has been added.
/// </returns>
/// <exception cref="ArgumentNullException">The <paramref name="handler"/> is <see langword="null"/>.</exception>
public UnhandledExceptionFilterAttribute Register<TException>(Func<Exception, HttpRequestMessage, HttpResponseMessage> handler)
where TException : Exception
{
if(handler == null)
{
throw new ArgumentNullException("handler");
}


var type    = typeof(TException);
var item    = new Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>(
null, handler
);


if (!this.Handlers.TryAdd(type, item))
{
Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> oldItem = null;


if (this.Handlers.TryRemove(type, out oldItem))
{
this.Handlers.TryAdd(type, item);
}
}


return this;
}
#endregion


#region Unregister<TException>()
/// <summary>
/// Unregisters the exception handler for exceptions of type <typeparamref name="TException"/>.
/// </summary>
/// <typeparam name="TException">The type of exception to unregister handlers for.</typeparam>
/// <returns>
/// This <see cref="UnhandledExceptionFilterAttribute"/> after the exception handler
/// for exceptions of type <typeparamref name="TException"/> has been removed.
/// </returns>
public UnhandledExceptionFilterAttribute Unregister<TException>()
where TException : Exception
{
Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> item = null;


this.Handlers.TryRemove(typeof(TException), out item);


return this;
}
#endregion
}
}

源代码也可以找到 给你

何时使用 HttpResponseException而不是 Response.CreateResponse(HttpStatusCode.NotFound)或其他错误状态代码的另一种情况是,如果您在操作过滤器中有事务,并且希望在向客户机返回错误响应时回滚事务。

使用 Response.CreateResponse不会回滚事务,而抛出异常则会回滚事务。

据我所知,是抛出异常还是返回 Request。CreateErrorResponse,结果是相同的。如果您查看 System 的源代码。韦伯。Http.dll,您将看到同样多的内容。看一下这个总结,以及我提出的一个非常类似的解决方案: Web Api、 HttpError 和异常的行为

我喜欢 反对意见

无论如何,我需要一种方法来捕捉继承的异常,而这个解决方案并不能满足我所有的需求。

所以我最终改变了他处理 OnException 的方式,这是我的版本

public override void OnException(HttpActionExecutedContext actionExecutedContext) {
if (actionExecutedContext == null || actionExecutedContext.Exception == null) {
return;
}


var type = actionExecutedContext.Exception.GetType();


Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> registration = null;


if (!this.Handlers.TryGetValue(type, out registration)) {
//tento di vedere se ho registrato qualche eccezione che eredita dal tipo di eccezione sollevata (in ordine di registrazione)
foreach (var item in this.Handlers.Keys) {
if (type.IsSubclassOf(item)) {
registration = this.Handlers[item];
break;
}
}
}


//se ho trovato un tipo compatibile, uso la sua gestione
if (registration != null) {
var statusCode = registration.Item1;
var handler = registration.Item2;


var response = handler(
actionExecutedContext.Exception.GetBaseException(),
actionExecutedContext.Request
);


// Use registered status code if available
if (statusCode.HasValue) {
response.StatusCode = statusCode.Value;
}


actionExecutedContext.Response = response;
}
else {
// If no exception handler registered for the exception type, fallback to default handler
actionExecutedContext.Response = DefaultHandler(actionExecutedContext.Exception.GetBaseException(), actionExecutedContext.Request
);
}
}

核心是这个循环,在这个循环中我检查异常类型是否是已注册类型的子类。

foreach (var item in this.Handlers.Keys) {
if (type.IsSubclassOf(item)) {
registration = this.Handlers[item];
break;
}
}

我的两分钱

如果您不返回 HttpResponseMessage,而是直接返回实体/模型类,我发现一种有用的方法是将以下实用函数添加到我的控制器中

private void ThrowResponseException(HttpStatusCode statusCode, string message)
{
var errorResponse = Request.CreateErrorResponse(statusCode, message);
throw new HttpResponseException(errorResponse);
}

并使用适当的状态代码和消息调用它

在出错的情况下,我想返回一个特定的错误详细信息类,不管客户端请求的是什么格式,而不是 Happy path 对象。

我想让我的控制器方法返回域特定的快乐路径对象,否则抛出一个异常。

我遇到的问题是 HttpResponseException 构造函数不允许域对象。

这是我最终想出来的

public ProviderCollection GetProviders(string providerName)
{
try
{
return _providerPresenter.GetProviders(providerName);
}
catch (BadInputValidationException badInputValidationException)
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest,
badInputValidationException.Result));
}
}

Result是一个包含错误详细信息的类,而 ProviderCollection是我的快乐路径结果。

我想指出的是,根据我的经验,如果在 webapi 2方法中抛出一个 HttpResponseException 而不是返回一个 HttpResponseMessage,那么如果一个调用被立即发送到 IIS Express,它将超时或返回一个200,但是在响应中有一个 html 错误。 最简单的测试方法是使 $。调用一个引发 HttpResponseException 的方法,并在 ajax 中的 errorCallBack 中立即调用另一个方法甚至一个简单的 http 页面。您将注意到立即调用将失败。如果在错误回调中添加一个断点或 settimeout () ,以延迟第二个调用一两秒钟,从而给服务器恢复正常工作的时间。这样就没有了,因为这几乎就像抛出 HttpResponseException 导致服务器端侦听器线程退出并重新启动,导致一瞬间没有服务器接受连接或其他东西。罢工

更新: 奇怪的 Ajax 连接 Timeout 的根本原因在于,如果调用 Ajax 的速度足够快,就会使用相同的 tcp 连接。我通过返回 HttpResonseMessage 或抛出返回到浏览器 ajax 调用的 HTTPResponseException 引发了一个401错误。但随着调用 MS 返回一个对象没有发现错误,因为在启动。Auth.vb 应用程序。启用了 UserCookieAuthentication,因此它试图返回拦截响应并添加重定向,但它错误地使用了 Object not Instance of an Object。这个错误是 html,但是在事实发生之后被附加到响应中,所以只有当 Ajax 调用足够快并且使用了相同的 tcp 连接时,它才会返回到浏览器中,然后被附加到下一个调用的前端。由于某些原因,Chrome 刚刚超时,由于 json 和 htm 的混合使用,小提琴手被淘汰了,但是火狐返回了真正的错误。很奇怪,但是数据包嗅探器或者火狐是唯一能找到这个的方法。

还应该注意的是,如果使用 Web API 帮助生成自动帮助并返回 HttpResponseMessage,则应该添加

[System.Web.Http.Description.ResponseType(typeof(CustomReturnedType))]

属性,以便帮助正确地生成。然后

return Request.CreateResponse<CustomReturnedType>(objCustomeReturnedType)

或者出错

return Request.CreateErrorResponse( System.Net.HttpStatusCode.InternalServerError, new Exception("An Error Ocurred"));

希望这可以帮助其他在抛出 HttpResponseException 之后可能出现随机超时或服务器不可用的人。

此外,返回 HttpResponseException 还有一个额外的好处: 当返回的错误是 AuthToken 需要在单个页面应用程序中刷新时,不会导致 Visual Studio 在未处理的异常上中断。

更新: 我正在收回关于 IIS Express 超时的声明,这恰好是我的客户端 Ajax 调用中的一个错误,原来是因为 Ajax 1.8返回 $。Ajax ()和返回 $。Ajax.().然后()都返回承诺而不是相同的链式承诺,然后()返回一个新的承诺,导致执行顺序错误。因此,当 then ()承诺完成时,它是一个脚本超时。奇怪的问题,但不是 IIS 表示问题之间的键盘和椅子的问题。

如果意图是使用 就是这个结果 结束请求,则不要抛出 HttpResponseException 或返回错误的 HttpResponesMessage-除了

HttpResponseException 的是 不能像处理其他异常一样处理。它们是 未被异常筛选器捕获。他们是 未被异常处理程序捕获。它们是在终止当前代码的执行流时插入 HttpResponseMessage 的狡猾方法。

除非代码是依赖于这种特殊的取消处理的基础结构代码,否则使用 HttpResponseException 类型的 避免

HttpResponseMessage 的不是例外。它们不会终止当前代码的执行流。它们可以作为异常过滤 没有。它们可以作为异常记录 没有。它们代表了一个有效的结果——甚至一个500响应也是“一个有效的非异常响应”!


让生活更简单:

当出现异常/错误情况时,继续并抛出一个标准。NET 异常-或定制的应用程序异常类型(从 HttpResponseException 派生的 没有) ,具有所需的“ http 错误/响应”属性,如状态代码-按照 正常的异常处理

使用异常过滤器/异常处理程序/异常记录器来处理这些异常情况: 更改/添加状态代码?添加跟踪标识符?包括堆栈痕迹?木头?

通过避免 HttpResponseException “例外情况”的处理是统一的,可以将其作为公开管道的一部分进行处理!例如,我们可以将“ NotFound”转换为404,将“ ArgumentException”转换为400,将“ NullReference”转换为500,这很容易,而且与应用程序级异常一致——同时允许扩展性提供“基础”,例如错误日志记录。