Выбросить HttpResponseException или вернуть Request.CreateErrorResponse?

После просмотра статьи Обработка исключений в веб-API ASP.NET Я немного смущен, когда бросать исключение vs возвращать ошибку ответ. Мне также интересно узнать, можно ли изменить ответ, когда ваш метод возвращает модель, специфичную для домена, вместо HttpResponseMessage...

Итак, напомню, вот мои вопросы, за которыми следует некоторый код с кодом #s:

Вопросы

Вопросы по делу № 1

  • Должен ли я всегда использовать HttpResponseMessage вместо конкретной модели домена, чтобы сообщение можно было настроить?
  • Можно ли настроить сообщение, если вы возвращаете конкретную модель домена?

Вопросы по делу № 2,3,4

  1. Должен ли я бросать исключение или возвращать ответ об ошибке? Если ответ "это зависит", можете ли вы дать ситуации/примеры того, когда использовать один против другого.
  2. В чем разница между бросанием HttpResponseException vs 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;
}

Update

Чтобы еще раз продемонстрировать случаи # 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);
}

Ответ 1

Подход, который я принял, - это просто исключить исключения из действий контроллера 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
    }
}

Исходный код также можно найти здесь.

Ответ 2

Если вы не возвращаете HttpResponseMessage и вместо этого возвращаете классы объектов/моделей напрямую, подход, который я нашел полезным, состоит в том, чтобы добавить следующую утилиту в мой контроллер

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

и просто вызовите его с соответствующим кодом состояния и сообщением

Ответ 3

Случай №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, но это не требуется. например, вы не хотите включать тело объекта в ответ, иначе вам может понадобиться другой формат.

Ответ 4

Не выкидывайте исключение HttpResponseException или возвращайте HttpResponesMessage для ошибок - кроме случаев, когда целью является завершение запроса с помощью этого точного результата.

HttpResponseException не обрабатываются так же, как и другие исключения. Они не попадают в фильтры исключений. Они не попадают в обработчик исключений. Это хитрый способ ускользнуть в HttpResponseMessage, завершая текущий поток выполнения кода.

Если код не является кодом инфраструктуры, основанным на этом специальном обращении, не используйте тип HttpResponseException!

HttpResponseMessage не являются исключениями. Они не прерывают текущий поток выполнения кода. Они не могут быть отфильтрованы как исключения. Они не могут быть зарегистрированы как исключения. Они представляют действительный результат - даже ответ 500 - "действительный ответ без исключения"!


Сделайте жизнь проще:

Если есть случай с исключительной/ошибочной ситуацией, перейдите в обычное исключение .NET или специальный тип исключения приложения (не из HttpResponseException) с требуемыми свойствами "HTTP error/response", такими как код состояния - как за обычную обработку исключений.

Использовать фильтры исключений/обработчики исключений/регистраторы исключений, чтобы сделать что-то подходящее в этих исключительных случаях: изменить/добавить коды состояния? добавить идентификаторы отслеживания? включить трассировки стека? войти?

Избегая исключения HttpResponseException, обработка "исключительного случая" становится единой и может обрабатываться как часть открытого конвейера! Например, можно превратить "NotFound" в 404 и "ArgumentException" в 400 и "NullReference" в 500 легко и единообразно с исключениями на уровне приложений, при этом позволяя расширяемость предоставлять "основы", такие как ведение журнала ошибок.

Ответ 5

Другим случаем использования HttpResponseException вместо Response.CreateResponse(HttpStatusCode.NotFound) или другого кода состояния ошибки является то, что если у вас есть транзакции в фильтрах действий и вы хотите, чтобы транзакции были отброшены при возврате ответа клиента клиенту.

Использование Response.CreateResponse не приведет к откату транзакции, тогда как исключение будет выбрано.

Ответ 6

Я хочу отметить, что мой опыт заключается в том, что если вы выбрали исключение HttpResponseException вместо возврата HttpResponseMessage в методе webapi 2, то, если сразу же будет отправлен вызов в IIS Express, он закроет или вернет 200 но с html-ошибкой в ​​ответе. Самый простой способ проверить это - сделать вызов $.ajax методу, который вызывает исключение HttpResponseException, а в errorCallBack в ajax - немедленный вызов другому методу или даже простой странице http. Вы заметите, что вызов imediate не удастся. Если вы добавите точку останова или setimeout() в обратном вызове, чтобы отложить второй вызов второй или второй, давая серверу время для восстановления, он работает правильно. Это не имеет никакого значения, но его почти как исключение HttpResponseException приводит к тому, что поток слушателей на стороне сервера выходит и перезапускается, вызывая разделение секунды, когда сервер не принимает соединения или что-то еще.Забастовкa >

Обновление: основная причина временного отключения Ajax Wierd - если вызов ajax выполняется достаточно быстро, используется одно и то же соединение tcp. Я поднимал 401 простой эфир ошибки, возвращая HttpResonseMessage или бросая исключение HTTPResponseException, которое было возвращено в браузер ajax-вызов. Но вместе с этим вызовом MS возвращалась ошибка Object Not Found, потому что в Startup.Auth.vb app.UserCookieAuthentication была включена, поэтому она пыталась вернуть перехват ответа и добавить перенаправление, но с ошибкой с объектом не экземпляр объекта. Эта ошибка была html, но была добавлена ​​к ответу после факта, поэтому, только если вызов ajax был сделан достаточно быстро, и одно и то же соединение tcp использовало его, оно было возвращено в браузер, а затем оно добавлено в начало следующего вызова. По какой-то причине Chrome только тайм-аут, скрипач вытащил из-за комбинации json и htm, но firefox превратил настоящую ошибку. Так что странный, но пакетный сниффер или firefox был единственным способом отследить это.

Также следует отметить, что если вы используете справку по веб-интерфейсу для создания автоматической справки и вы возвращаете 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 имеет дополнительное преимущество, заключающееся в том, что Visual Studio не разбивается на исключение без обработки, когда возвращается ошибка: AuthToken необходимо обновить в приложении с одной страницей.

Обновление: я оттягиваю свое утверждение о сроках выхода IIS Express, это было ошибкой на моей стороне клиента. ajax call back получается, поскольку Ajax 1.8 возвращает $.ajax() и возвращает $.ajax.(). then() оба возвращают обещание, но не одно и то же, что и обещание, тогда() возвращает новое обещание, которое вызвало неправильный порядок выполнения. Поэтому, когда завершение then() завершилось, это был тайм-аут script. Weird gotcha, но не проблема с IIS, проблема между клавиатурой и стулом.

Ответ 7

Насколько я могу судить, вы бросаете исключение или возвращаете Request.CreateErrorResponse, результат тот же. Если вы посмотрите на исходный код System.Web.Http.dll, вы увидите столько же. Взгляните на это общее резюме и очень похожее решение, которое я сделал: Web Api, HttpError и поведение исключений

Ответ 8

Мне нравится Ответ оппозиции

В любом случае мне нужен способ поймать унаследованное исключение, и это решение не удовлетворяет всем моим потребностям

поэтому я в конечном итоге изменил способ обработки 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

Ответ 9

В ситуациях с ошибкой я хотел вернуть конкретный класс сведений об ошибках в любом формате, запрошенном клиентом, вместо объекта счастливого пути.

Я хочу, чтобы мои методы-контроллеры возвращали объект счастливого пути для домена, а иначе выделяли исключение.

Проблема была в том, что конструкторы 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 - результат моего счастливого пути.