Que devrait retourner un service JSON en cas d'échec / d'erreur?

je suis en train d'écrire un JSON service en C# (.ashx fichier). Sur une demande réussie au service je renvoie quelques données JSON. Si la requête échoue, soit parce qu'une exception a été lancée (par exemple le délai d'expiration de la base de données), soit parce que la requête était erronée d'une certaine manière (par exemple un ID qui n'existe pas dans la base de données a été donné comme argument), comment le service devrait-il répondre? Quels sont les codes de statut HTTP qui sont sensés, et devrais-je renvoyer des données, le cas échéant?

Je m'attends à ce que le service être principalement appelé de jQuery en utilisant le jQuery.form plugin, est-ce que jQuery ou ce plugin ont un moyen par défaut de gérer une réponse d'erreur?

EDIT: j'ai décidé d'utiliser jQuery + .ashx + HTTP [codes d'état] en cas de succès, je renvoie JSON mais en cas d'erreur, je renvoie une chaîne de caractères, car il semble que c'est l'option d'erreur pour jQuery.ajax attend.

73
demandé sur thatismatt 2009-03-23 19:15:49

11 réponses

le code de statut HTTP que vous renvoyez doit dépendre du type d'erreur qui s'est produite. Si un ID n'existe pas dans la base de données, renvoie un 404; si un utilisateur n'a pas assez de privilèges pour faire cet appel Ajax, renvoie un 403; si la base de données est épuisée avant de pouvoir trouver l'enregistrement, renvoie un 500 (Erreur du serveur).

jQuery détecte automatiquement ces codes d'erreur et exécute la fonction de rappel que vous définissez dans votre appel Ajax. Document: http://api.jquery.com/jQuery.ajax /

petit exemple de $.ajax appel d'erreur:

$.ajax({
  type: 'POST',
  url: '/some/resource',
  success: function(data, textStatus) {
    // Handle success
  },
  error: function(xhr, textStatus, errorThrown) {
    // Handle error
  }
});
32
répondu Ron DeVera 2015-09-07 21:19:09

voir cette question pour un aperçu des meilleures pratiques pour votre situation.

la suggestion topline (à partir de ce lien) est de standardiser une structure de réponse (à la fois pour le succès et l'échec) que votre gestionnaire recherche, en saisissant toutes les Exceptions à la couche serveur et en les convertissant à la même structure. Par exemple (de cette réponse ):

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

C'est l'approche utilisations de stackoverflow (dans le cas où vous vous demandiez comment d'autres font ce genre de chose); écrire des opérations comme le vote ont "Success" et "Message" champs, indépendamment de si le vote a été autorisé ou non:

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

Comme @Phil.H a souligné , vous devez être cohérent dans tout ce que vous choisissez. C'est plus facile à dire qu'à faire (tout est dans le de développement!).

par exemple, si vous soumettez des commentaires trop rapidement sur SO, au lieu d'être cohérent et de revenir

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

va donc lancer une exception de serveur ( HTTP 500 ) et l'attraper dans leur callback error .

autant qu'il" se sent bien "d'utiliser jQuery + .ashx + HTTP [codes de statut] IMO il va ajouter plus de complexité à votre base de code côté client que ce qu'il vaut. Réalisez que jQuery ne "détecte" pas les codes d'erreur mais plutôt l'absence d'un code de succès. Cette distinction est importante lorsque j'essaie de concevoir un client autour des codes de réponse http avec jQuery. Vous n'avez que deux choix (c'était un "succès" ou "erreur"?), vous devez branche sur votre propre. Si vous avez un petit nombre de WebServices pilotant un petit nombre de pages alors il pourrait être correct, mais n'importe quoi de plus grande échelle peut devenir salissant.

il est beaucoup plus naturel dans un service Web .asmx (ou WCF d'ailleurs) de retourner un objet personnalisé que de personnaliser le code de statut HTTP. De Plus, vous avez la La sérialisation JSON pour gratuit.

54
répondu Crescent Fresh 2017-05-23 12:00:25

utiliser des codes de statut HTTP serait une façon reposante de le faire, mais cela suggérerait de rendre le reste de l'interface reposante en utilisant URIs ressource et ainsi de suite.

en vérité, définissez l'interface comme vous voulez (retournez un objet d'erreur, par exemple, détaillant la propriété avec l'erreur, et un morceau de HTML qui l'explique, etc), mais une fois que vous avez décidé sur quelque chose qui fonctionne dans un prototype, être impitoyablement cohérent.

16
répondu Phil H 2009-03-23 16:18:46

je pense que si vous faites juste une exception, elle devrait être traitée dans le callback jQuery qui est passé pour l'option" erreur " . (Nous enregistrons également cette exception du côté du serveur dans un journal central). Aucun code D'erreur HTTP spécial N'est requis, mais je suis curieux de voir ce que les autres font aussi.

C'est ce que je fais, mais c'est juste mes dollars.02

si vous devez vous reposer et retourner les codes d'erreur, essayez de vous en tenir à la codes standard définis par le W3C: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

3
répondu Dan Esparza 2009-03-23 16:24:34

j'ai passé des heures à résoudre ce problème. Ma solution est basée sur les souhaits/exigences suivants:

  • il n'y a pas de code de traitement des erreurs répétitives dans toutes les actions du contrôleur JSON.
  • préserve les codes D'état HTTP (error). Pourquoi? Parce que les préoccupations de plus haut niveau ne devraient pas affecter la mise en œuvre de niveau inférieur.
  • soit capable d'obtenir des données JSON lorsqu'une erreur/exception se produit sur le serveur. Pourquoi? Parce Que Je pourriez riche en informations sur l'erreur. Par exemple: message d'erreur, code d'état d'erreur spécifique au domaine, trace de la pile (dans l'environnement de débogage/développement).
  • facilité d'utilisation côté client - préférable en utilisant jQuery.

je crée une HandleErrorAttribute (voir les commentaires du code pour l'explication des détails). Quelques détails incluant "usings" ont été omis, donc le code pourrait ne pas être compilé. J'ajoute le filtre aux filtres globaux lors de l'initialisation de l'application dans Mondial.asax.comme ceci:

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

attribut:

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 /q/how-to-find-in-which-controller-action-an-error-occurred-75163/"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;
    }
  }
}

j'ai développé le plugin suivant jQuery pour la facilité d'utilisation côté client:

(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);

Qu'est-ce que j'obtiens de tout ça? Le résultat final est que

  • aucune de mes actions de contrôleur n'a d'exigences sur HandleErrorAttributes.
  • aucune de mes actions de contrôleur ne contient de Plaque de chaudière répétitive code de traitement des erreurs.
  • j'ai un seul code de traitement des points d'erreur qui me permet de changer facilement les opérations de journalisation et autres choses liées au traitement des erreurs.
  • une exigence simple: les actions du contrôleur retournant JsonResult doivent avoir le type de retour JsonResult et non un type de base comme ActionResult. Motif: voir le Commentaire du code dans FooHandleErrorAttribute.

exemple côté Client:

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

commentaires sont la plupart de bienvenue! Je vais probablement bloguer sur cette solution un jour...

3
répondu Bjarke 2012-08-03 07:58:04

je retournerais certainement une erreur 500 avec un objet JSON décrivant la condition d'erreur, similaire à comment ASP.NET AJAX" ScriptService "error returns . Je crois que c'est assez standard. Il est certainement agréable d'avoir cette cohérence lorsque vous manipulez des conditions d'erreur potentiellement inattendues.

mis à part, pourquoi ne pas simplement utiliser la fonctionnalité intégrée dans .NET, si vous l'écrivez en C#? Les services WCF et ASMX facilitent la sérialisation des données comme JSON, sans réinventer la roue.

2
répondu Dave Ward 2009-03-23 16:55:56

Rails échafaudages utiliser 422 Unprocessable Entity pour ces types d'erreurs. Pour en savoir plus, lisez RFC 4918 .

2
répondu ZiggyTheHamster 2011-05-27 15:29:46

Oui, vous devez utiliser les codes de statut HTTP. Et aussi de préférence retourner des descriptions d'erreur dans un format JSON quelque peu standardisé, comme proposition de Nottingham , voir rapport d'erreur d'apigilité :

la charge utile d'un problème API a la structure suivante:

  • type : URL d'un document décrivant la condition d'erreur (optionnel), et "about:blank" est supposé si aucun n'est fourni; doit se résoudre à un lisible document; Apigility toujours fournit des ce).
  • titre : un titre bref pour la condition d'erreur (requis; et devrait être le même pour chaque problème du même type ; L'Apigilité fournit toujours ceci).
  • statut : le code de statut HTTP pour le requête courante (optionnel; Apigility le fournit toujours).
  • détail : détails d'erreur spécifiques à cette requête (facultatif; L'Apigilité l'exige pour chaque problème.)
  • instance : URI identifiant l'instance spécifique de ce problème (optionnel; actualité ne le permet pas).
2
répondu mb21 2016-01-15 10:07:30

si l'utilisateur fournit des données invalides, il doit certainement s'agir d'une 400 Bad Request ( la requête contient une mauvaise syntaxe ou ne peut pas être remplie. )

1
répondu Daniel Serodio 2009-08-05 22:40:45

Je ne pense pas que vous devriez retourner des codes d'erreur http, plutôt des exceptions personnalisées qui sont utiles à l'extrémité client de l'application pour que l'interface sache ce qui s'est réellement produit. Je n'essaierais pas de masquer de vrais problèmes avec des codes d'erreur 404 ou quelque chose de ce genre.

0
répondu Quintin Robinson 2009-03-23 16:19:37

pour les erreurs de serveur / protocole j'essaierais D'être aussi REST/HTTP QUE POSSIBLE (comparez ceci avec vous en tapant des URL dans votre navigateur):

  • non existant élément est appelé (/personnes/{non-existant-id-ici}). Renvoie un 404.
  • une erreur inattendue sur le serveur (bug de code) s'est produite. De retour de 500.
  • l'utilisateur client n'est pas autorisé à obtenir la ressource. Retournez un 401.

pour domaine / logique d'entreprise erreurs spécifiques je dirais que le protocole est utilisé de la bonne manière et il n'y a pas d'erreur interne au serveur, alors répondre par une erreur JSON / XML objet ou ce que vous préférez décrire vos données avec (comparez cela avec vous remplir des formulaires sur un site web):

  • un Utilisateur souhaite changer son nom de compte, mais il n'a pas encore vérifié son compte en cliquant sur un lien dans un courriel envoyé à l'utilisateur. Return {"error":"Compte vérifié"} ou quoi.
  • un utilisateur veut commander un livre, mais le livre a été vendu (état changé en DB) et ne peut plus être commandé. Return {"error":"Livre déjà vendu"}.
0
répondu Almer 2013-07-18 09:31:54