首页 文章

抛出HttpResponseException还是返回Request.CreateErrorResponse?

提问于
浏览
162

在审阅了一篇文章Exception Handling in ASP.NET Web API后,我对于何时抛出异常与返回错误响应感到有点困惑 . 我还想知道当你的方法返回特定于域的模型而不是 HttpResponseMessage 时是否可以修改响应...

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

问题

关于案例#1的问题

  • 我是否应该始终使用 HttpResponseMessage 而不是具体的域模型,以便可以自定义消息?

  • 如果要返回具体的域模型,是否可以自定义消息?

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

  • 我应该抛出异常还是返回错误响应?如果答案是"it depends",你能否给出关于何时使用一个与另一个的情况/例子 .

  • 投掷 HttpResponseExceptionRequest.CreateErrorResponse 有什么区别?输出到客户端似乎相同......

  • 我是否应始终在错误中使用 HttpError 到"wrap"响应消息(是否抛出异常或返回错误响应)?

案例样本

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

9 回答

  • 0

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

  • 13

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

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

  • 3

    我想指出根据我的经验,如果抛出HttpResponseException而不是在webapi 2方法中返回HttpResponseMessage,那么如果立即对IIS Express进行调用,它将超时或返回200但响应中出现html错误 . 最简单的测试方法是对抛出HttpResponseException的方法进行$ .ajax调用,并在ajax的errorCallBack中立即调用另一个方法甚至是一个简单的http页面 . 您会注意到中间呼叫将失败 . 如果在错误回调中添加断点或settimeout()以延迟第二次或第二次调用,使服务器有时间恢复,则可以正常工作 . 这从来没有,但它几乎像throw HttpResponseException导致服务器端侦听器线程退出并重新启动导致一秒钟没有服务器接受连接或什么 .

    更新:奇怪的Ajax连接Timeout的根本原因是如果使用相同的tcp连接足够快地进行ajax调用 . 我通过返回一个HttpResonseMessage或抛出一个返回给浏览器ajax调用的HTTPResponseException来引发401错误 . 但随着该调用MS返回一个Object Not Found Error,因为在Startup.Auth.vb app.UserCookieAuthentication已启用,因此它试图返回拦截响应并添加重定向,但它错误地使用了Object而不是Object的实例 . 这个错误是html但是事后附加到响应中,所以只有当ajax调用足够快并且使用相同的tcp连接时它才会返回到浏览器然后它被附加到下一个调用的前面 . 出于某种原因,Chrome只是时间限制,因为json和htm混合使用了fiddler,但firefox掀起了真正的错误 . 如此奇怪,但数据包嗅探器或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在未处理的异常中断 .

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

  • 7

    情况1

    • 不一定,管道中还有其他地方可以修改响应(动作过滤器,消息处理程序) .

    • 见上文 - 但如果操作返回域模型,则无法修改操作内的响应 .

    案例#2-4

    • 抛出HttpResponseException的主要原因是:

    • 如果您要返回域模型但需要处理错误情况,

    • 通过将错误视为异常来简化控制器逻辑

    • 这些应该是等价的; 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;
    }
    

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

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

    Do not throw an HttpResponseException or return an HttpResponesMessage for errors - except if the intent is to end the request with that exact result .

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

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

    HttpResponseMessage 's are not exceptions. They do not terminate the current code'的执行流程 . 它们不能作为例外进行过滤 . 它们不能作为例外记录 . 它们代表了有效的结果 - 即使500响应也是"a valid non-exception response"!


    让生活更简单:

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

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

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

  • 99

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

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

    我遇到的问题是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 是我喜欢的路径结果 .

  • 0

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

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

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

  • 0

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

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

    使用此过滤器可以实现集中式异常处理,而不是将其分布在控制器操作中 . 但是,有些情况下,我会在控制器操作中捕获异常,如果集中处理该特定异常没有意义,则返回特定响应 .

    Example registration of 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 class:

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

    源代码也可以找到here .

  • 23

    我喜欢Oppositional answer

    无论如何,我需要一种方法来捕获继承的Exception,并且该解决方案无法满足我的所有需求 .

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

相关问题