web-dev-qa-db-ja.com

HttpResponseExceptionをスローするか、Request.CreateErrorResponseを返しますか?

記事を確認した後 ASP.NET Web APIの例外処理 例外をスローするタイミングとエラーレスポンスを返すタイミングについて少し混乱しています。また、メソッドがHttpResponseMessage...の代わりにドメイン固有のモデルを返す場合、応答を変更できるかどうか疑問に思っています。

したがって、ここで要約すると、ケース番号のあるコードが続く私の質問です:

ご質問

ケース#1に関する質問

  1. メッセージをカスタマイズできるように、具体的なドメインモデルの代わりに常にHttpResponseMessageを使用する必要がありますか?
  2. 具体的なドメインモデルを返す場合、メッセージをカスタマイズできますか?

ケース#2,3,4に関する質問

  1. 例外をスローするか、エラー応答を返す必要がありますか?答えが「依存する」の場合、どちらを使用するかについての状況/例を挙げてください。
  2. HttpResponseExceptionRequest.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);
}
167
zam6ak

私が取ったアプローチは、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
    }
}

ソースコードも見つけることができます here

102
Oppositional

HttpResponseMessageを返さず、代わりにエンティティ/モデルクラスを直接返す場合、次のユーティリティ関数をコントローラーに追加するのが便利であることがわかりました

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

適切なステータスコードとメッセージで呼び出します

23
Joe King

事例#1

  1. 必ずしも、応答を変更する他の場所がパイプライン内にある(アクションフィルター、メッセージハンドラー)。
  2. 上記を参照してください。ただし、アクションがドメインモデルを返す場合、応答を変更できませんinsideアクション。

ケース#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 /などにシリアル化できますが、必須ではありません。たとえば、応答にエンティティ本体を含めたくない場合や、他の形式が必要な場合があります。

15
Mike Wasson

HttpResponseExceptionをスローしたり、エラーに対してHttpResponesMessageを返したりしない-except意図がendである場合その正確な結果

HttpResponseExceptionは他の例外と同じように処理されません。それらは、例外フィルターではキャッチされません。それらは、例外ハンドラーでキャッチされない。これらは、現在のコードの実行フローを終了しながらHttpResponseMessageをスリップするtoな方法です。

コードがこの特別なアンハンドリングに依存するインフラストラクチャコードでない限り、avoidHttpResponseExceptionタイプを使用してください!

HttpResponseMessageは例外ではありません。現在のコードの実行フローは終了しません。例外としてnotフィルターできます。例外としてnotログに記録できます。これらは有効な結果を表します-500応答であっても「有効な非例外応答」です!


人生をもっとシンプルに:

例外/エラーが発生した場合、通常の.NET例外をスローするか、またはカスタマイズされたアプリケーション例外タイプ(notHttpResponseExceptionから派生)を目的の 'httpエラー/レスポンスでスローします'ステータスコードなどのプロパティ-通常の例外処理に従って.

例外フィルター/例外ハンドラー/例外ロガーを使用して、これらの例外的なケースで適切な処理を行います。ステータスコードを変更/追加しますか?追跡識別子を追加しますか?スタックトレースを含めますか?ログ?

HttpResponseExceptionを回避することにより、「例外的なケース」の処理が均一になり、公開されたパイプラインの一部として処理できます!たとえば、アプリケーションレベルの例外を使用して、「NotFound」を404に、「ArgumentException」を400に、「NullReference」を500に簡単かつ均一に変換できますが、エラーロギングなどの「基本」を提供できます。

14
user2864740

Response.CreateResponse(HttpStatusCode.NotFound)やその他のエラーステータスコードの代わりにHttpResponseExceptionを使用する場合のもう1つのケースは、アクションフィルターにトランザクションがあり、クライアントにエラー応答を返すときにトランザクションをロールバックする場合です。

Response.CreateResponseを使用してもトランザクションはロールバックされませんが、例外をスローするとロールバックされます。

7
Rob Gray

Webapi 2メソッドでHttpResponseMessageを返す代わりにHttpResponseExceptionをスローした場合、IIS Expressの呼び出しがすぐに行われた場合、タイムアウトまたは200を返すことは私の経験でした。ただし、応答にhtmlエラーが含まれています。これをテストする最も簡単な方法は、HttpResponseExceptionをスローするメソッドへの$ .ajax呼び出しを行い、ajaxのerrorCallBackで別のメソッドまたは単純なhttpページへの即時呼び出しを行うことです。即時呼び出しが失敗することがわかります。エラーコールバックにブレークポイントまたはsettimeout()を追加して、2番目の呼び出しを1〜2秒遅らせて、サーバーが回復する時間を与えて正常に動作するようにします。これは何もしませんが、throw HttpResponseExceptionに似ているため、サーバー側のリスナースレッドが終了して再起動し、サーバーが接続などを受け入れないという一瞬を引き起こします。

更新:異常なAjax接続タイムアウトの根本的な原因は、同じtcp接続が使用されるajax呼び出しが十分に速く行われた場合です。 HttpResonseMessageを返すか、ブラウザーのajax呼び出しに返されたHTTPResponseExceptionをスローして、401エラーを発生させていました。ただし、その呼び出しとともに、Startup.Auth.vbでapp.UserCookieAuthenticationが有効になっているため、MSはObject Not Foundエラーを返していたため、応答をインターセプトしてリダイレクトを追加しようとしましたが、オブジェクトのインスタンスではなくオブジェクトでエラーが発生しました。このエラーはhtmlでしたが、事実の後に応答に追加されたため、ajax呼び出しが十分に速く行われ、同じtcp接続が使用された場合にのみブラウザに返され、次の呼び出しの前に追加されました。何らかの理由でChromeがタイムアウトになったため、フィドラーはjsonとhtmの混合のためにパックしましたが、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 1.8が$ .ajax()を返し、$。ajaxを返すことから判明した、クライアント側のajaxコールバックの誤りでした。 ().then()は両方ともpromiseを返しますが、同じチェーンのpromiseを返しませんthen()は、実行の順序を誤った新しいpromiseを返します。そのため、then()promiseが完了すると、スクリプトのタイムアウトになりました。 IIS Expressの問題ではなく、キーボードと椅子の間に問題があります。

3
Andrew DeVries

反対の答え が好き

とにかく、継承された例外をキャッチする方法が必要でしたが、そのソリューションは私のすべてのニーズを満たしていません。

だから私は彼が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

0
Fabio Angela

エラー状況では、ハッピーパスオブジェクトの代わりに、クライアントが要求した形式で特定のエラー詳細クラスを返したいと思いました。

コントローラーメソッドがドメイン固有のハッピーパスオブジェクトを返し、それ以外の場合は例外をスローするようにします。

私が抱えていた問題は、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
NickBeaugié

私が知る限り、例外をスローするか、Request.CreateErrorResponseを返すかにかかわらず、結果は同じです。 System.Web.Http.dllのソースコードを見ると、同じことがわかります。この一般的な概要と、私が作成した非常によく似たソリューションをご覧ください。 Web Api、HttpError、および例外の動作

0
Andy Cohen