抛出HttpResponseException还是返回Request.CreateErrorResponse?

zam*_*6ak 171 c# exception-handling http-status-codes asp.net-web-api

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

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

问题

关于案例#1的问题

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

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

  1. 我应该抛出异常还是返回错误响应?如果答案是"它取决于",你能否提供关于何时使用一个与另一个的情况/例子.
  2. 投掷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;
}
Run Code Online (Sandbox Code Playgroud)

更新

为了帮助进一步演示案例#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);
}
Run Code Online (Sandbox Code Playgroud)

Opp*_*nal 102

我采用的方法是从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);
            }
        }
    )
);
Run Code Online (Sandbox Code Playgroud)

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
    }
}
Run Code Online (Sandbox Code Playgroud)

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


Joe*_*ing 23

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

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

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

  • 这是正确的答案,它带有格式"消息"作为正文中的键值对.这通常是我看到其他框架和语言的作用 (4认同)

Mik*_*son 15

情况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;
    }
    
    Run Code Online (Sandbox Code Playgroud)

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

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


use*_*740 15

不要抛出HttpResponseException或返回HttpResponesMessage以查找错误 - 除非意图是使用该确切结果结束请求.

HttpResponseException的处理方式与其他异常不同.它们不会被异常过滤器捕获.它们不会被异常处理程序捕获.在终止当前代码的执行流程时,它们是在HttpResponseMessage中滑动的狡猾方式.

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

HttpResponseMessage也不例外.它们不会终止当前代码的执行流程.它们不能作为例外进行过滤.它们不能作为例外记录.它们代表了有效的结果 - 即使500响应也是"有效的非异常响应"!


让生活更简单:

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

使用异常过滤器/异常处理程序/异常记录器来执行适合这些特殊情况的操作:更改/添加状态代码?添加跟踪标识符?包括堆栈跟踪?登录?

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

  • 我理解为什么控制器中的ArgumentException在逻辑上会是400,但是`ArgumentException在堆栈中更深?将这些转换为400并不一定是正确的,但是如果你有一个过滤器将所有`ArgumentException转换为400,那么避免这种情况的唯一方法是在控制器中捕获异常并重新抛出其他内容,这似乎打破了过滤器或类似过程中统一异常处理的目的. (2认同)

Rob*_*ray 8

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

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