web-dev-qa-db-fra.com

Que doit renvoyer un service JSON en cas d'échec / d'erreur

J'écris un service JSON en C # (fichier .ashx). Sur une demande réussie au service, je retourne des données JSON. Si la demande échoue, soit parce qu'une exception a été levée (par exemple, un délai d'attente de la base de données), soit parce que la demande était erronée (par exemple, un ID qui n'existe pas dans la base de données a été donné en argument), comment le service devrait-il répondre? Quels codes d’état HTTP sont utiles et dois-je renvoyer des données, le cas échéant?

Je prévois que le service sera principalement appelé à partir de jQuery à l'aide du plugin jQuery.form. Est-ce que jQuery ou ce plugin dispose d'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 renverrai JSON, mais en cas d'erreur, je renverrai une chaîne, car il apparaît que c'est qu'attend l'option d'erreur pour jQuery.ajax.

78
thatismatt

Le code d'état HTTP que vous renvoyez doit dépendre du type d'erreur survenu. Si un identifiant n'existe pas dans la base de données, renvoyez un 404; si un utilisateur n'a pas assez de privilèges pour passer cet appel Ajax, retournez un 403; si la base de données arrive à expiration avant de pouvoir retrouver l'enregistrement, renvoyez 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. Documentation: http://api.jquery.com/jQuery.ajax/

Petit exemple d'un $.ajax _ rappel d'erreur:

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

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

La suggestion de dessus (à partir dudit lien) consiste à normaliser une structure de réponse (succès et échec) recherchée par votre gestionnaire, en capturant toutes les exceptions au niveau du serveur et en les convertissant dans 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 utilisée par stackoverflow (au cas où vous vous demanderiez comment les autres font ce genre de chose); écrire des opérations comme voter a "Success" et "Message" _ champs, que le vote soit autorisé ou non:

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

Comme @ Phil.H l'a souligné , vous devriez être cohérent dans ce que vous choisissez. C’est plus facile à dire qu’à faire (comme pour tout ce qui est en développement!).

Par exemple, si vous envoyez des commentaires trop rapidement sur SO, au lieu d’être cohérents et de renvoyer

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

SO lancera une exception de serveur (HTTP 500) et attrapez-le dans leur callback error.

Autant qu'il est "correct" d'utiliser jQuery + .ashx + HTTP [codes d'état] IMO, cela ajoutera plus de complexité à votre base de code côté client que cela ne vaut la peine. Sachez que jQuery ne "détecte" pas les codes d'erreur, mais plutôt l'absence de code de réussite. C'est une distinction importante lorsque vous essayez de concevoir un client autour de codes de réponse http avec jQuery. Vous ne disposez que de deux choix (était-ce un "succès" ou une "erreur"?), Que vous devez poursuivre plus loin. Si vous avez un petit nombre de WebServices conduisant un petit nombre de pages, c'est peut-être acceptable, mais tout ce qui est à plus grande échelle risque de devenir compliqué.

C'est beaucoup plus naturel dans un .asmx WebService (ou WCF d'ailleurs) pour renvoyer un objet personnalisé plutôt que pour personnaliser le code d'état HTTP. De plus, vous obtenez gratuitement la sérialisation JSON.

55
Crescent Fresh

Utiliser des codes de statut HTTP serait un moyen de le faire, mais cela suggérerait de rendre le reste de l'interface RESTful à l'aide d'URI de ressources, etc.

En vérité, définissez l'interface à votre guise (retournez un objet d'erreur, par exemple, en détaillant la propriété avec l'erreur, et une partie du code HTML qui l'explique, etc.), mais une fois que vous avez décidé de quelque chose qui fonctionne dans un prototype , soyez impitoyablement cohérent.

17
Phil H

Je pense que si vous faites simplement une bulle avec une exception, elle devrait être gérée dans le callback jQuery qui est passé pour l'option 'error' . (Nous enregistrons également cette exception côté serveur dans un journal central). Aucun code d'erreur HTTP spécial n'est requis, mais je suis curieux de voir ce que font les autres.

C'est ce que je fais, mais ce n'est que mon 0,02 $

Si vous voulez être RESTful et renvoyer des codes d'erreur, essayez de vous en tenir aux codes standard définis par le W3C: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html =

3
Dan Esparza

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

  • Ne pas avoir de code de traitement d'erreur répétitif passe-partout dans toutes les actions du contrôleur JSON.
  • Conservez les codes d'état HTTP (d'erreur). Pourquoi? Parce que les préoccupations de niveau supérieur ne devraient pas affecter la mise en œuvre de niveau inférieur.
  • Pouvoir obtenir des données JSON lorsqu'une erreur/exception se produit sur le serveur. Pourquoi? Parce que je pourrais vouloir des informations d'erreur riches. Par exemple. message d'erreur, code d'état d'erreur spécifique au domaine, trace de pile (dans un environnement de débogage/développement).
  • Facilité d'utilisation côté client - préférable avec jQuery.

Je crée un HandleErrorAttribute (voir les commentaires de code pour une explication des détails). Quelques détails, y compris les "utilisations", ont été omis, le code pourrait donc ne pas être compilé. J'ajoute le filtre aux filtres globaux lors de l'initialisation de l'application dans Global.asax.cs 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 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;
    }
  }
}

J'ai développé le plugin jQuery suivant 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 je tire 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 code de traitement d'erreur de plaque de chaudière répétitif.
  • J'ai un code unique de gestion des erreurs qui me permet de modifier facilement la journalisation et d'autres tâches liées à la gestion des erreurs.
  • Une exigence simple: les actions du contrôleur renvoyant JsonResult doivent avoir le type de retour JsonResult et non un type de base comme ActionResult. Raison: voir le commentaire de 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);

Les commentaires sont les bienvenus! Je vais probablement bloguer à propos de cette solution un jour ...

3
Bjarke

Oui, vous devez utiliser les codes de statut HTTP. Et renvoyez également de préférence les descriptions d’erreur dans un format JSON quelque peu standardisé, tel que proposition de Nottingham , voir Rapport d’erreur de apigility :

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

  • type : une URL vers un document décrivant la condition d'erreur (facultatif, et "à propos de: blanc" est supposé si aucun n'est fourni; doit être résolu en = lisible par l'homme document; Apigility fournit toujours cela).
  • title : bref titre de la condition d'erreur (obligatoire; il devrait être identique pour chaque problème de la même manière type ; Apigility fournit toujours cela).
  • status : code de statut HTTP de la requête en cours (facultatif; Apigility le fournit toujours).
  • detail : détails de l'erreur spécifiques à cette requête (facultatif; Apigility le requiert pour chaque problème).
  • instance : adresse URI identifiant l'instance spécifique de ce problème (facultatif; Apigility ne le fournit pas actuellement).
2
mb21

Je voudrais certainement retourner une erreur 500 avec un objet JSON décrivant la condition d'erreur, semblable à comment une erreur ASP.NET AJAX "ScriptService" retourne) . Je crois que c'est est Il est certainement agréable d’avoir cette cohérence lors du traitement de conditions d’erreur potentiellement inattendues.

De plus, pourquoi ne pas simplement utiliser les fonctionnalités intégrées de .NET, si vous les écrivez en C #? Les services WCF et ASMX facilitent la sérialisation des données au format JSON, sans réinventer la roue.

2
Dave Ward

Les échafaudages de rails utilisent 422 Unprocessable Entity pour ces types d’erreurs. Voir RFC 4918 pour plus d'informations.

2
ZiggyTheHamster

Si l'utilisateur fournit des données non valides, il doit s'agir définitivement d'un 400 Bad Request (La requête contient une syntaxe incorrecte ou ne peut pas être remplie.)

1
Daniel Serodio

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

0
Quintin Robinson

Pour les erreurs de serveur/de protocole, j'essaierais d'être aussi REST/HTTP que possible (comparez ceci avec vous en tapant l'URL dans votre navigateur):

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

Pour les erreurs spécifiques à la logique de domaine/métier, je dirais que le protocole est utilisé correctement et qu'il n'y a pas d'erreur interne au serveur. Répondez donc avec un objet JSON/XML d'erreur ou ce que vous préférez pour décrire vos données (comparez cela avec votre saisie formulaires sur un site web):

  • un utilisateur souhaite modifier le nom de son compte, mais l'utilisateur n'a pas encore vérifié son compte en cliquant sur un lien figurant dans un courrier électronique qui lui a été envoyé. Renvoie {"erreur": "Compte non vérifié"} ou autre.
  • un utilisateur veut commander un livre, mais le livre a été vendu (l'état a été modifié dans DB) et ne peut plus être commandé. Retourne "" erreur ":" Livre déjà vendu "}.
0
Almer