183

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

所以,在这里回顾一下我的问题,然后是一些带有案例#s的代码:

问题

关于案例 #1 的问题

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

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

  1. 我应该抛出异常还是返回错误响应?如果答案是“视情况而定”,您能否提供有关何时使用一种与另一种的情况/示例。
  2. throwing HttpResponseExceptionvs 和有什么不一样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);
}
4

9 回答 9

109

我采用的方法是从 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);
            }
        }
    )
);

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

源代码也可以在这里找到。

于 2012-09-21T15:34:08.470 回答
28

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

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

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

于 2014-12-05T13:11:53.067 回答
19

不要抛出 HttpResponseException 或为错误返回 HttpResponesMessage -除非意图以该确切结果结束请求

HttpResponseException 的处理方式与其他异常不同。它们没有被 Exception Filters 捕获。它们没有被 Exception Handler 捕获。它们是在终止当前代码的执行流时插入 HttpResponseMessage 的一种狡猾的方法。

除非代码是依赖这种特殊处理的基础设施代码,否则请避免使用 HttpResponseException 类型!

HttpResponseMessage 也不例外。它们不会终止当前代码的执行流程。它们不能作为例外过滤。它们不能被记录为异常。它们代表一个有效的结果——即使是 500 响应也是“有效的非异常响应”!


让生活更简单:

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

使用异常过滤器/异常处理程序/异常记录器对这些异常情况做一些适当的事情:更改/添加状态代码?添加跟踪标识符?包括堆栈跟踪?日志?

通过避免 HttpResponseException ,“异常情况”处理变得统一,并且可以作为暴露管道的一部分进行处理!例如,可以使用应用程序级异常轻松统一地将“NotFound”转换为 404,将“ArgumentException”转换为 400,将“NullReference”转换为 500 - 同时允许可扩展性以提供“基础”,例如错误日志记录。

于 2016-11-13T20:11:22.670 回答
16

情况1

  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/等,但这不是必需的。例如,您可能不想在响应中包含实体主体,或者您可能需要其他格式。

于 2012-09-20T22:56:47.093 回答
9

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

usingResponse.CreateResponse不会回滚事务,而抛出异常会。

于 2013-02-04T23:47:55.963 回答
3

我想指出,根据我的经验,如果在 webapi 2 方法中抛出 HttpResponseException 而不是返回 HttpResponseMessage,如果立即对 IIS Express 进行调用,它将超时或返回 200 但出现 html 错误响应。最简单的测试方法是对抛出 HttpResponseException 的方法进行 $.ajax 调用,并在 ajax 中的 errorCallBack 中立即调用另一个方法,甚至是一个简单的 http 页面。您会注意到立即呼叫将失败。如果您在错误回调中添加断点或 settimeout() 以将第二次调用延迟一两秒,从而使服务器有时间恢复它可以正常工作。

更新:奇怪的 Ajax 连接超时的根本原因是,如果使用相同的 tcp 连接足够快地进行 ajax 调用。我通过返回 HttpResonseMessage 或抛出返回给浏览器 ajax 调用的 HTTPResponseException 引发了 401 错误。但是随着该调用,MS 返回了一个未找到对象错误,因为在 Startup.Auth.vb app.UserCookieAuthentication 已启用,因此它试图返回拦截响应并添加重定向,但它错误的是对象而不是对象的实例。此错误是 html,但在事后附加到响应中,因此只有当 ajax 调用足够快并且使用相同的 tcp 连接时,它才会返回到浏览器,然后附加到下一次调用的前面。由于某种原因,Chrome 刚刚超时,由于 json 和 htm 的混合,提琴手 pucked,但 firefox rturned 真正的错误。如此奇怪,但数据包嗅探器或火狐是追踪这个的唯一方法。

另外应该注意的是,如果您使用 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 后随机超时或服务器不可用的人。

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

更新:我正在撤回关于 IIS Express 超时的声明,这恰好是我的客户端 ajax 回调中的一个错误,因为 Ajax 1.8 返回 $.ajax() 并返回 $.ajax.().then()两者都返回承诺,但不是相同的链接承诺 then() 返回一个导致执行顺序错误的新承诺。所以当 then() 承诺完成时,它是一个脚本超时。奇怪的问题,但不是 IIS express 问题,是键盘和椅子之间的问题。

于 2015-06-10T15:39:17.917 回答
0

据我所知,无论是抛出异常,还是返回 Request.CreateErrorResponse,结果都是一样的。如果您查看 System.Web.Http.dll 的源代码,您会看到很多。看看这个一般总结,以及我做出的一个非常相似的解决方案:Web Api, HttpError, and the behavior of exceptions

于 2013-06-06T03:39:55.473 回答
0

我喜欢反对的答案

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

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

my2cents

于 2013-07-11T13:02:03.827 回答
0

在错误情况下,我想以客户端请求的任何格式而不是快乐路径对象返回特定的错误详细信息类。

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

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

于 2015-03-10T17:08:07.747 回答