web-dev-qa-db-ja.com

JSONサービスは失敗/エラー時に何を返すべきか

私はC#(.ashxファイル)でJSONサービスを書いています。サービスへのリクエストが成功すると、JSONデータを返します。例外がスローされた(データベースタイムアウトなど)か、何らかの方法でリクエストが間違っていた(データベースに存在しないIDが引数として指定されたなど)ためにリクエストが失敗した場合、サービスはどのように応答する必要がありますか?どのHTTPステータスコードが賢明であり、もしあればデータを返すべきですか?

私はサービスが主にjQuery.formプラグインを使用してjQueryから呼び出されると予想していますが、jQueryまたはこのプラグインにはエラー応答を処理するデフォルトの方法がありますか?

編集: jQuery + .ashx + HTTP [ステータスコード]を使用すると、成功するとJSONが返されますが、エラーが発生すると文字列が返されます。 jQuery.ajaxのエラーオプションが期待するもの。

78
thatismatt

返されるHTTPステータスコードは、発生したエラーの種類によって異なります。 IDがデータベースに存在しない場合、404を返します。ユーザーがそのAjax呼び出しを行うのに十分な特権を持っていない場合、403を返します。レコードを見つける前にデータベースがタイムアウトした場合、500(サーバーエラー)を返します。

jQueryはこのようなエラーコードを自動的に検出し、Ajax呼び出しで定義したコールバック関数を実行します。ドキュメント: http://api.jquery.com/jQuery.ajax/

$.ajaxエラーコールバックの短い例:

$.ajax({
  type: 'POST',
  url: '/some/resource',
  success: function(data, textStatus) {
    // Handle success
  },
  error: function(xhr, textStatus, errorThrown) {
    // Handle error
  }
});
33
Ron DeVera

この質問 を参照して、状況に応じたベストプラクティスの洞察を確認してください。

(上記のリンクからの)トップラインの提案は、ハンドラーが探す応答構造(成功と失敗の両方)を標準化し、サーバー層ですべての例外をキャッチし、それらを同じ構造に変換することです。例えば(from this answer ):

{
    success:false,
    general_message:"You have reached your max number of Foos for the day",
    errors: {
        last_name:"This field is required",
        mrn:"Either SSN or MRN must be entered",
        zipcode:"996852 is not in Bernalillo county. Only Bernalillo residents are eligible"
    }
} 

これはstackoverflowが使用するアプローチです(他の人がこの種のことをどのように行うのか疑問に思っている場合)。投票などの書き込み操作には"Success"および"Message"フィールド、投票が許可されたかどうかに関係なく:

{ Success:true, NewScore:1, Message:"", LastVoteTypeId:3 }

@ Phil.Hが指摘した のように、あなたが選んだものは何でも一貫しているべきです。これは、開発中のすべてのことと同様、言うよりも簡単です。

たとえば、SOでコメントを送信するのが速すぎる場合、一貫性を持たずに戻る場合

{ Success: false, Message: "Can only comment once every blah..." }

SOはサーバー例外(HTTP 500errorコールバックでキャッチします。

JQuery + .ashx + HTTP [ステータスコード] IMOを使用すると、クライアント側のコードベースが価値以上に複雑になります。 jQueryはエラーコードを「検出」せず、成功コードがないことを認識してください。これは、jQueryを使用してhttp応答コードを中心にクライアントを設計しようとする際の重要な違いです。選択肢は2つしかありません(「成功」または「エラー」でしたか?)、さらに自分で分岐する必要があります。少数のページを駆動する少数のWebServiceがある場合は問題ないかもしれませんが、大規模なものは面倒になります。

.asmx WebService(またはWCF)は、HTTPステータスコードをカスタマイズするよりもカスタムオブジェクトを返します。さらに、JSONシリアル化を無料で入手できます。

55
Crescent Fresh

HTTPステータスコードを使用するのはRESTfulな方法ですが、リソースURIなどを使用して残りのインターフェイスをRESTfulにすることをお勧めします。

実際には、必要に応じてインターフェイスを定義します(たとえば、エラーオブジェクト、エラーのプロパティの詳細、エラーを説明するHTMLのチャンクなど)を定義しますが、プロトタイプで機能するものを決定したら、情け容赦なく一貫している。

17
Phil H

例外をバブルするだけであれば、 「エラー」オプションに渡されるjQueryコールバック で処理する必要があると思います。 (この例外はサーバー側で中央ログに記録されます)。特別なHTTPエラーコードは必要ありませんが、他の人たちが何をしているのか知りたいです。

これは私がやることですが、それはちょうど私の$ .02です

RESTfulでエラーコードを返す場合は、W3Cで規定されている標準コードに固執してください。 http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html =

3
Dan Esparza

私はこの問題を解決するのに数時間費やしました。私のソリューションは、次の要望/要件に基づいています。

  • すべてのJSONコントローラーアクションに反復的なボイラープレートエラー処理コードを含めないでください。
  • HTTP(エラー)ステータスコードを保持します。どうして?上位レベルの懸念が下位レベルの実装に影響を与えないためです。
  • サーバーでエラー/例外が発生したときにJSONデータを取得できるようにします。どうして?豊富なエラー情報が必要な場合があるため。例えば。エラーメッセージ、ドメイン固有のエラーステータスコード、スタックトレース(デバッグ/開発環境)。
  • クライアント側の使いやすさ-jQueryを使用することをお勧めします。

HandleErrorAttributeを作成します(詳細については、コードのコメントを参照してください)。 「用途」を含むいくつかの詳細は省略されているため、コードはコンパイルされない可能性があります。 Global.asax.csでのアプリケーションの初期化中に、このようにグローバルフィルターにフィルターを追加します。

GlobalFilters.Filters.Add(new UnikHandleErrorAttribute());

属性:

namespace Foo
{
  using System;
  using System.Diagnostics;
  using System.Linq;
  using System.Net;
  using System.Reflection;
  using System.Web;
  using System.Web.Mvc;

  /// <summary>
  /// Generel error handler attribute for Foo MVC solutions.
  /// It handles uncaught exceptions from controller actions.
  /// It outputs trace information.
  /// If custom errors are enabled then the following is performed:
  /// <ul>
  ///   <li>If the controller action return type is <see cref="JsonResult"/> then a <see cref="JsonResult"/> object with a <c>message</c> property is returned.
  ///       If the exception is of type <see cref="MySpecialExceptionWithUserMessage"/> it's message will be used as the <see cref="JsonResult"/> <c>message</c> property value.
  ///       Otherwise a localized resource text will be used.</li>
  /// </ul>
  /// Otherwise the exception will pass through unhandled.
  /// </summary>
  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
  public sealed class FooHandleErrorAttribute : HandleErrorAttribute
  {
    private readonly TraceSource _TraceSource;

    /// <summary>
    /// <paramref name="traceSource"/> must not be null.
    /// </summary>
    /// <param name="traceSource"></param>
    public FooHandleErrorAttribute(TraceSource traceSource)
    {
      if (traceSource == null)
        throw new ArgumentNullException(@"traceSource");
      _TraceSource = traceSource;
    }

    public TraceSource TraceSource
    {
      get
      {
        return _TraceSource;
      }
    }

    /// <summary>
    /// Ctor.
    /// </summary>
    public FooHandleErrorAttribute()
    {
      var className = typeof(FooHandleErrorAttribute).FullName ?? typeof(FooHandleErrorAttribute).Name;
      _TraceSource = new TraceSource(className);
    }

    public override void OnException(ExceptionContext filterContext)
    {
      var actionMethodInfo = GetControllerAction(filterContext.Exception);
      // It's probably an error if we cannot find a controller action. But, hey, what should we do about it here?
      if(actionMethodInfo == null) return;

      var controllerName = filterContext.Controller.GetType().FullName; // filterContext.RouteData.Values[@"controller"];
      var actionName = actionMethodInfo.Name; // filterContext.RouteData.Values[@"action"];

      // Log the exception to the trace source
      var traceMessage = string.Format(@"Unhandled exception from {0}.{1} handled in {2}. Exception: {3}", controllerName, actionName, typeof(FooHandleErrorAttribute).FullName, filterContext.Exception);
      _TraceSource.TraceEvent(TraceEventType.Error, TraceEventId.UnhandledException, traceMessage);

      // Don't modify result if custom errors not enabled
      //if (!filterContext.HttpContext.IsCustomErrorEnabled)
      //  return;

      // We only handle actions with return type of JsonResult - I don't use AjaxRequestExtensions.IsAjaxRequest() because ajax requests does NOT imply JSON result.
      // (The downside is that you cannot just specify the return type as ActionResult - however I don't consider this a bad thing)
      if (actionMethodInfo.ReturnType != typeof(JsonResult)) return;

      // Handle JsonResult action exception by creating a useful JSON object which can be used client side
      // Only provide error message if we have an MySpecialExceptionWithUserMessage.
      var jsonMessage = FooHandleErrorAttributeResources.Error_Occured;
      if (filterContext.Exception is MySpecialExceptionWithUserMessage) jsonMessage = filterContext.Exception.Message;
      filterContext.Result = new JsonResult
        {
          Data = new
            {
              message = jsonMessage,
              // Only include stacktrace information in development environment
              stacktrace = MyEnvironmentHelper.IsDebugging ? filterContext.Exception.StackTrace : null
            },
          // Allow JSON get requests because we are already using this approach. However, we should consider avoiding this habit.
          JsonRequestBehavior = JsonRequestBehavior.AllowGet
        };

      // Exception is now (being) handled - set the HTTP error status code and prevent caching! Otherwise you'll get an HTTP 200 status code and running the risc of the browser caching the result.
      filterContext.ExceptionHandled = true;
      filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; // Consider using more error status codes depending on the type of exception
      filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);

      // Call the overrided method
      base.OnException(filterContext);
    }

    /// <summary>
    /// Does anybody know a better way to obtain the controller action method info?
    /// See http://stackoverflow.com/questions/2770303/how-to-find-in-which-controller-action-an-error-occurred.
    /// </summary>
    /// <param name="exception"></param>
    /// <returns></returns>
    private static MethodInfo GetControllerAction(Exception exception)
    {
      var stackTrace = new StackTrace(exception);
      var frames = stackTrace.GetFrames();
      if(frames == null) return null;
      var frame = frames.FirstOrDefault(f => typeof(IController).IsAssignableFrom(f.GetMethod().DeclaringType));
      if (frame == null) return null;
      var actionMethod = frame.GetMethod();
      return actionMethod as MethodInfo;
    }
  }
}

クライアント側の使いやすさのために、次のjQueryプラグインを開発しました。

(function ($, undefined) {
  "using strict";

  $.FooGetJSON = function (url, data, success, error) {
    /// <summary>
    /// **********************************************************
    /// * UNIK GET JSON JQUERY PLUGIN.                           *
    /// **********************************************************
    /// This plugin is a wrapper for jQuery.getJSON.
    /// The reason is that jQuery.getJSON success handler doesn't provides access to the JSON object returned from the url
    /// when a HTTP status code different from 200 is encountered. However, please note that whether there is JSON
    /// data or not depends on the requested service. if there is no JSON data (i.e. response.responseText cannot be
    /// parsed as JSON) then the data parameter will be undefined.
    ///
    /// This plugin solves this problem by providing a new error handler signature which includes a data parameter.
    /// Usage of the plugin is much equal to using the jQuery.getJSON method. Handlers can be added etc. However,
    /// the only way to obtain an error handler with the signature specified below with a JSON data parameter is
    /// to call the plugin with the error handler parameter directly specified in the call to the plugin.
    ///
    /// success: function(data, textStatus, jqXHR)
    /// error: function(data, jqXHR, textStatus, errorThrown)
    ///
    /// Example usage:
    ///
    ///   $.FooGetJSON('/foo', { id: 42 }, function(data) { alert('Name :' + data.name); }, function(data) { alert('Error: ' + data.message); });
    /// </summary>

    // Call the ordinary jQuery method
    var jqxhr = $.getJSON(url, data, success);

    // Do the error handler wrapping stuff to provide an error handler with a JSON object - if the response contains JSON object data
    if (typeof error !== "undefined") {
      jqxhr.error(function(response, textStatus, errorThrown) {
        try {
          var json = $.parseJSON(response.responseText);
          error(json, response, textStatus, errorThrown);
        } catch(e) {
          error(undefined, response, textStatus, errorThrown);
        }
      });
    }

    // Return the jQueryXmlHttpResponse object
    return jqxhr;
  };
})(jQuery);

これから何が得られますか?最終結果は

  • コントローラーアクションには、HandleErrorAttributesに関する要件はありません。
  • コントローラーアクションには、ボイラープレートのエラー処理コードの繰り返しは含まれていません。
  • エラー処理コードの単一ポイントがあるので、ロギングやその他のエラー処理関連のものを簡単に変更できます。
  • 単純な要件:JsonResultを返すコントローラーアクションは、ActionResultのような基本型ではなく、JsonResultの戻り値型を持つ必要があります。理由:FooHandleErrorAttributeのコードコメントを参照してください。

クライアント側の例:

var success = function(data) {
  alert(data.myjsonobject.foo);
};
var onError = function(data) {
  var message = "Error";
  if(typeof data !== "undefined")
    message += ": " + data.message;
  alert(message);
};
$.FooGetJSON(url, params, onSuccess, onError);

コメントは大歓迎です!おそらくいつかこのソリューションについてブログを書くでしょう...

3
Bjarke

はい、HTTPステータスコードを使用する必要があります。また、できれば Nottinghamの提案 のような多少標準化されたJSON形式でエラーの説明を返すこともできます。 apigility Error Reporting を参照してください。

API問題のペイロードの構造は次のとおりです。

  • type:エラー状態を説明するドキュメントへのURL(オプション、および何も提供されない場合は「about:blank」が想定されます。 human-readable document; Apigilityは常にこれを提供します)。
  • title:エラー状態の簡単なタイトル(必須。同じの問題ごとに同じである必要があります) type; Apigilityは常にこれを提供します)。
  • status:現在のリクエストのHTTPステータスコード(オプション。Apigilityは常にこれを提供します)。
  • detail:このリクエストに固有のエラーの詳細(オプション。Apigilityは問題ごとにそれを要求します)。
  • instance:この問題の特定のインスタンスを識別するURI(オプション。Apigilityは現在これを提供していません)。
2
mb21

ASP.NET AJAX "ScriptService"エラーが返す方法 と同様に、エラー条件を記述するJSONオブジェクトで500エラーを確実に返します。これはかなり標準的で、潜在的に予期しないエラー状態を処理するときに一貫性があることは間違いなく素晴らしいことです。

それに、C#で記述している場合、.NETの組み込み機能を使用しないのはなぜですか。 WCFおよびASMXサービスにより、車輪を再発明することなく、データをJSONとして簡単にシリアル化できます。

2
Dave Ward

Railsの足場は422 Unprocessable Entityこれらの種類のエラー。詳細については、 RFC 4918 を参照してください。

2
ZiggyTheHamster

ユーザーが無効なデータを提供する場合、400 Bad Requestリクエストに不正な構文が含まれているか、実行できません。

1
Daniel Serodio

インターフェースが実際に何が起こったかを知るために、アプリケーションのクライアント側にとって有用なカスタム例外ではなく、HTTPエラーコードを返すべきではないと思います。私は、404エラーコードまたはその性質に関する実際の問題を隠そうとはしません。

0

サーバー/プロトコルエラーの場合、可能な限りREST/HTTPを使用するようにします(ブラウザーでURLを入力する場合と比較してください)。

  • 存在しないアイテムは(/ persons/{non-existing-id-here})と呼ばれます。 404を返します。
  • サーバーで予期しないエラー(コードバグ)が発生しました。 500を返します。
  • クライアントユーザーはリソースを取得する権限がありません。 401を返します。

ドメイン/ビジネスロジック固有のエラーの場合、プロトコルは正しい方法で使用され、サーバーの内部エラーは発生しないと言うので、エラーJSON/XMLオブジェクトまたはデータを記述したいものは何でも応答します(これを記入して比較してください)ウェブサイト上のフォーム):

  • ユーザーはアカウント名を変更したいが、ユーザーに送信されたメール内のリンクをクリックしてもまだアカウントを確認しなかった。 {"error": "アカウントが検証されていません"}などを返します。
  • ユーザーが本を注文したいが、その本は販売された(DBで状態が変更された)ため、もう注文することはできません。 {"error": "本はすでに販売されています"}を返します。
0
Almer