183

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

したがって、ここで要約すると、私の質問の後にケース番号を含むコードが続きます。

質問

ケース#1に関する質問

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

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

  1. 例外をスローするか、エラー応答を返す必要がありますか? 答えが「場合による」の場合は、どちらを使用するか、いつ使用するかについて状況/例を挙げていただけますか。
  2. HttpResponseException投げる と投げるはどう違い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 に答える
19

エラーに対して HttpResponseException をスローしたり、HttpResponesMessage を返したりしないでください。ただし、正確な結果でリクエストを終了することが意図されている場合を除きます。

HttpResponseException は、他の例外と同じように処理されません。それらはException Filters で捕捉されません。それらはException Handler でキャッチされません。これらは、現在のコードの実行フローを終了させながら HttpResponseMessage を挿入するずるい方法です。

コードがこの特殊な un-handling に依存するインフラストラクチャ コードでない限り、HttpResponseException 型の使用は避けてください。

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


生活をよりシンプルに:

例外/エラーのケースがある場合は、通常の例外に従って、通常の .NET 例外、またはステータス コードなどの必要な「http エラー/応答」プロパティを持つカスタマイズされたアプリケーション例外タイプ (HttpResponseException から派生していない) をスローします。取扱説明

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

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

于 2016-11-13T20:11:22.670 に答える
16

ケース #1

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

ケース #2-4

  1. HttpResponseException をスローする主な理由は次のとおりです。
    • ドメイン モデルを返しているが、エラー ケースを処理する必要がある場合、
    • エラーを例外として扱うことでコントローラのロジックを簡素化する
  2. これらは同等である必要があります。HttpResponseException は、HTTP 応答として返される HttpResponseMessage をカプセル化します。

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

于 2012-09-20T22:56:47.093 に答える
9

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

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

于 2013-02-04T23:47:55.963 に答える
3

私の経験では、webapi 2 メソッドで HttpResponseMessage を返す代わりに HttpResponseException をスローすると、IIS Express への呼び出しがすぐに行われると、タイムアウトするか 200 が返されますが、html エラーが発生します。応答。これをテストする最も簡単な方法は、$.ajax で HttpResponseException をスローするメソッドを呼び出し、ajax の errorCallBack で別のメソッドまたは単純な http ページを即座に呼び出すことです。即時呼び出しが失敗することがわかります。エラー コールバックにブレーク ポイントまたは settimeout() を追加して、2 番目の呼び出しを 1 秒または 2 秒遅らせると、サーバーは正常に動作します。

アップデート:奇妙な Ajax 接続タイムアウトの根本的な原因は、ajax 呼び出しが十分に迅速に行われた場合に、同じ TCP 接続が使用されることです。HttpResonseMessage を返すか、ブラウザーの ajax 呼び出しに返された HTTPResponseException をスローして、401 エラー イーサを発生させていました。しかし、その呼び出しに伴い、MS は Object Not Found エラーを返しました。これは、Startup.Auth.vb で app.UserCookieAuthentication が有効になっているため、応答をインターセプトしてリダイレクトを追加しようとしたが、オブジェクトのインスタンスではなくオブジェクトでエラーが発生したためです。このエラーは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.().then() を返すため、クライアント側の ajax コールバックの間違いでした。どちらも promise を返しますが、同じチェーンされた promise ではありません then() は新しい promise を返すため、実行順序が間違っていました。したがって、 then() promise が完了すると、スクリプトのタイムアウトになりました。奇妙な落とし穴ですが、キーボードと椅子の間の問題を IIS が明示的に発行するわけではありません。

于 2015-06-10T15:39:17.917 に答える
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 に答える
0

私の知る限り、例外をスローしても Request.CreateErrorResponse を返しても、結果は同じです。System.Web.Http.dll のソース コードを見ると、多くのことがわかります。この一般的な要約と、私が作成した非常によく似た解決策を見てください: Web Api、HttpError、および例外の動作

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

私の2セント

于 2013-07-11T13:02:03.827 に答える