抛出HttpResponseException或返回Request.CreateErrorResponse?

在审查了ASP.NET Web API中的一个文章exception处理之后,我对于何时抛出一个exception与返回一个错误响应有点混淆。 我还想知道,当你的方法返回一个域特定的模型而不是HttpResponseMessage时,是否可以修改响应…

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

问题

案例#1的问题

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

案件#2,3,4的问题

  1. 我应该抛出一个exception还是返回错误响应? 如果答案是“取决于”,你可以给出关于何时使用一个和另一个的情况/例子。
  2. 抛出HttpResponseExceptionRequest.CreateErrorResponse什么区别? 输出到客户端似乎相同…
  3. 我应该总是使用HttpError来“包装”错误的响应消息(无论是抛出exception还是返回错误响应)?

案例样本

 // 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,下面的代码片段突出了几个选项,当没有find客户时可以“发生”…

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

我采取的方法是从api控制器操作中引发exception,并注册exceptionfilter来处理exception,并在操作执行上下文中设置适当的响应。

该filter公开了一个stream畅的接口,它提供了一种方法,在注册具有全局configuration的filter之前为特定types的exception注册处理程序。

使用此filter可以实现集中的exception处理,而不是将其分散在控制器操作中。 但是,有些情况下我会在控制器操作中捕获exception,并在集中处理特定exception时没有意义的情况下返回特定的响应。

filter的注册示例:

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

UnhandledExceptionFilterAttribute类:

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

源代码也可以在这里find。

如果你没有返回HttpResponseMessage ,而是直接返回实体/模型类,我发现有用的方法是添加下面的实用function到我的控制器

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

只需用相应的状态码和消息来调用它

情况1

  1. 不一定,pipe道中还有其他地方可以修改响应(动作filter,消息处理程序)。
  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; } 

    …但是如果你的控制器逻辑比较复杂,抛出exception可能会简化代码stream。

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

不要抛出一个HttpResponseException或返回一个HttpResponesMessage错误 – 除非意图是结束具有该确切结果 的请求

HttpResponseException的处理方式与其他例外情况不同 。 它们不被捕获到Exception Filters中 。 他们没有被抓到exception处理程序 。 在终止当前代码的执行stream程时,它们是在HttpResponseMessage中滑入的一种狡猾的方式。

除非代码是依赖于这个特殊的未处理的基础结构代码,否则请避免使用HttpResponseExceptiontypes!

HttpResponseMessage不是例外。 它们不会终止当前代码的执行stream程。 他们不能被过滤为例外。 他们不能被logging为例外。 它们代表了一个有效的结果 – 即使500个回答是“一个有效的非例外回应”!


让生活更简单:

当出现exception/错误情况时,请按照正常的exception情况,抛出正常的.NETexception – 或者定制的应用程序exceptiontypes( 不是从HttpResponseException派生),并带有期望的“http error / response”属性处理

使用exceptionfilter/exception处理程序/exceptionlogging器来处理这些exception情况:更改/添加状态代码? 添加跟踪标识符? 包括堆栈跟踪? login?

通过避免HttpResponseException exception处理是统一的 ,可以作为暴露pipe道的一部分来处理! 例如,可以将一个“NotFound”变成一个404,一个“ArgumentException”变成一个400,一个“NullReference”变成一个500,并且与应用程序级别的exception一致 – 同时允许扩展性提供诸如错误logging等“基础”。

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

使用Response.CreateResponse将不会回滚事务,而抛出exception。

我想指出,这是我的经验,如果抛出一个HttpResponseException而不是在webapi 2方法中返回一个HttpResponseMessage,如果一个调用立即到IIS Express它将超时或返回一个200,但有一个HTML错误响应。 testing这个最简单的方法是使$ ajax调用引发HttpResponseException的方法,并在ajax中的errorCallBack中立即调用另一个方法甚至简单的http页面。 你会注意到imediate通话将失败。 如果在错误callback中添加断点或settimeout(),以便延迟第二个呼叫一两秒钟,以便服务器恢复时间,可以正常工作。 这使得没有,但它几乎像抛出HttpResponseException导致服务器端侦听器线程退出并重新启动造成一秒钟没有服务器接受连接或东西。

更新:错误的Ajax连接超时的根本原因是如果ajax调用足够快,使用相同的tcp连接。 我正在通过返回一个HttpResonseMessage或抛出一个返回给浏览器ajax调用的HTTPResponseException来引发401错误。 但随着这个调用MS返回一个对象未​​发现错误,因为在Startup.Auth.vb app.UserCookieAuthentication启用,所以它试图返回拦截响应,并添加一个redirect,但它与对象的对象不实例的错误。 这个错误是HTML的,但事后追加到响应,所以只有当ajax调用足够快,并使用相同的tcp连接返回到浏览器,然后它被追加到下一个调用的前面。 出于某种原因,Chrome只是超时,由于json和htm的混合而导致的提琴手,但是firefox产生了真正的错误。 所以更奇怪,但数据包嗅探器或火狐是跟踪这一个唯一的方法。

还应该注意的是,如果您使用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在未处理的exception中有用。

更新:我收回我关于IIS Express的声明超时,这发生在我的客户端ajaxcallback它自从Ajax 1.8返回$ .ajax()并返回$ .ajax。()。then()既返回承诺,但不是相同的链接承诺然后()返回一个新的承诺,导致执行的顺序是错误的。 所以当then()承诺完成时,它是一个脚本超时。 怪异的疑难杂症,但不是一个IIS快递问题之间的键盘和椅子。

据我所知,无论是抛出一个exception,还是返回Request.CreateErrorResponse,结果都是一样的。 如果你看看System.Web.Http.dll的源代码,你会看到很多。 看一下这个总结,以及我所做的非常类似的解决scheme: Web Api,HttpError和exception行为

我喜欢对立的答案

无论如何,我需要一种方法来捕获inheritanceexception,该解决scheme并不能满足我所有的需求

所以我最终改变了如何处理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 ); } } 

核心是这个循环,我检查如果exceptiontypes是一个注册types的子类

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

my2cents

在错误的情况下,我想返回一个特定的错误细节类,以客户端请求的任何格式而不是快乐path对象。

我想让我的控制器方法返回特定于域的快乐path对象,否则抛出exception。

我遇到的问题是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是我的快乐path结果。