Que devrait retourner un service JSON en cas d’échec ou d’erreur

J’écris un service JSON en C # (fichier .ashx). En cas de demande réussie au service, je renvoie des données JSON. Si la demande échoue, soit parce qu’une exception a été émise (par exemple, délai d’attente de la firebase database) ou parce que la requête était incorrecte (par exemple, un ID qui n’existe pas dans la firebase database), comment le service devrait-il répondre? Quels codes d’état HTTP sont sensés et devrais-je renvoyer des données, le cas échéant?

Je prévois que le service sera principalement appelé depuis jQuery en utilisant le plugin jQuery.form, jQuery ou ce plugin a-t-il 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 retournerai JSON mais en cas d’erreur, je retournerai une chaîne, car il semble que c’est l’option d’erreur pour jQuery. ajax attend.

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

Exemple court d’un rappel d’erreur $.ajax :

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

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

La suggestion topline (à partir dudit lien) consiste à normaliser une structure de réponse (à la fois pour le succès et l’échec) que votre gestionnaire recherche, en capturant toutes les exceptions au niveau de la couche 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); les opérations d’écriture telles que le vote ont des champs "Success" et "Message" , 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 tout ce que vous choisissez. C’est plus facile à dire qu’à faire (comme tout est en développement!).

Par exemple, si vous soumettez des commentaires trop rapidement sur SO, au lieu d’être cohérent et de retourner

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

SO lancera une exception de serveur ( HTTP 500 ) et l’interceptera dans son rappel d’ error .

Autant qu’il “semble correct” d’utiliser jQuery + .ashx + HTTP [codes d’état] IMO cela appenda 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 n’avez que deux choix (était-ce un “succès” ou une “erreur”?), Que vous devez approfondir vous-même. Si vous avez un petit nombre de WebServices pilotant un petit nombre de pages, cela peut être correct, mais tout changement à grande échelle peut être compliqué.

Il est beaucoup plus naturel dans un WebService (ou WCF) de renvoyer un object personnalisé que de personnaliser le code d’état HTTP. De plus, vous obtenez la sérialisation JSON gratuitement.

Utiliser les codes de statut HTTP serait un moyen RESTful de le faire, mais cela suggérerait de rendre le rest de l’interface RESTful en utilisant des URI de ressources, etc.

En vérité, définissez l’interface à votre guise (retournez un object 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 choisi quelque chose qui fonctionne dans un prototype , soyez impitoyablement cohérent.

Je pense que si vous faites juste une exception, il devrait être traité dans le rappel jQuery transmis pour l’option “erreur” . (Nous enregistrons également cette exception côté serveur dans un journal central). Aucun code d’erreur HTTP particulier n’est requirejs, mais je suis curieux de voir ce que font les autres.

C’est ce que je fais, mais c’est juste mon $ .02

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

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 répétitif de gestion des erreurs dans toutes les actions du contrôleur JSON.
  • Préserve les codes d’état HTTP (erreur). Pourquoi? Parce que les préoccupations de niveau supérieur ne devraient pas affecter la mise en œuvre de niveau inférieur.
  • Être capable d’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 la stack (dans l’environnement de débogage / développement).
  • Facilité d’utilisation côté client – préférable d’utiliser jQuery.

Je crée un HandleErrorAtsortingbute (voir les commentaires de code pour une explication des détails). Quelques détails, y compris “usings”, ont été omis, donc le code peut 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 UnikHandleErrorAtsortingbute()); 

Atsortingbut:

 namespace Foo { using System; using System.Diagnostics; using System.Linq; using System.Net; using System.Reflection; using System.Web; using System.Web.Mvc; ///  /// Generel error handler atsortingbute 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: /// 
    ///
  • If the controller action return type is then a object with a message property is returned. /// If the exception is of type it's message will be used as the message property value. /// Otherwise a localized resource text will be used.
  • ///
/// Otherwise the exception will pass through unhandled. ///
[AtsortingbuteUsage(AtsortingbuteTargets.Class | AtsortingbuteTargets.Method)] public sealed class FooHandleErrorAtsortingbute : HandleErrorAtsortingbute { private readonly TraceSource _TraceSource; /// /// must not be null. /// /// public FooHandleErrorAtsortingbute(TraceSource traceSource) { if (traceSource == null) throw new ArgumentNullException(@"traceSource"); _TraceSource = traceSource; } public TraceSource TraceSource { get { return _TraceSource; } } /// /// Ctor. /// public FooHandleErrorAtsortingbute() { var className = typeof(FooHandleErrorAtsortingbute).FullName ?? typeof(FooHandleErrorAtsortingbute).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 = ssortingng.Format(@"Unhandled exception from {0}.{1} handled in {2}. Exception: {3}", controllerName, actionName, typeof(FooHandleErrorAtsortingbute).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 = FooHandleErrorAtsortingbuteResources.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); } /// /// 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. /// /// /// 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 ssortingct"; $.FooGetJSON = function (url, data, success, error) { ///  /// ********************************************************** /// * 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 (ie 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); }); ///  // 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 reçois de tout ça? Le résultat final est que

  • Aucune de mes actions de contrôleur ne nécessite des conditions sur HandleErrorAtsortingbutes.
  • Aucune de mes actions de contrôleur ne contient de code répétitif de traitement des erreurs de la plaque de la chaudière.
  • Je dispose d’un code de gestion des erreurs unique qui me permet de modifier facilement la journalisation et d’autres éléments liés à la gestion des erreurs.
  • Une exigence simple: les actions du contrôleur renvoyant celles de JsonResult doivent avoir le type de retour JsonResult et non un type de base tel que ActionResult. Raison: Voir le commentaire de code dans FooHandleErrorAtsortingbute.

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 sur cette solution un jour …

Je retournerais certainement une erreur 500 avec un object JSON décrivant la condition d’erreur, similaire à la façon dont une erreur ASP.NET AJAX “ScriptService” retourne . Je crois que c’est assez standard. Il est très intéressant d’avoir cette cohérence lors de la gestion de conditions d’erreur potentiellement inattendues.

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

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

Oui, vous devez utiliser les codes de statut HTTP. De plus, renvoyez de préférence des descriptions d’erreur dans un format JSON quelque peu normalisé, comme la proposition de Nottingham , voir la section Rapport d’erreurs sur l’aigilité :

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

  • type : une URL vers un document décrivant la condition d’erreur (facultatif, et “about: blank” est supposé si aucun n’est fourni; doit être résolu en un document lisible par l’homme ; Apigility le fournit toujours).
  • title : un titre bref pour la condition d’erreur (obligatoire; devrait être le même pour chaque problème du même type ; Apigility le fournit toujours).
  • status : le code d’état HTTP de la requête en cours (facultatif; Apigility le fournit toujours).
  • detail : détails d’erreur spécifiques à cette requête (facultatif; Apigility l’exige pour chaque problème).
  • instance : URI identifiant l’instance spécifique de ce problème (facultatif; Apigility ne le fournit pas actuellement).

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

Je ne pense pas que vous devriez renvoyer 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 passé. Je n’essaierais pas de masquer des problèmes réels avec des codes d’erreur 404 ou quelque chose de ce genre.

Pour les erreurs de serveur / protocole, j’essaierais d’être aussi REST / HTTP que possible (comparez avec votre saisie d’URL dans votre navigateur):

  • un élément non existant est appelé (/ persons / {non-existant-id-ici}). Retourne 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. Retourne 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’ya pas d’erreur interne du serveur. Répondez donc avec un object JSON / XML ou ce que vous préférez pour décrire vos données. formulaires sur un site web):

  • un utilisateur souhaite changer son nom de compte mais l’utilisateur n’a pas encore vérifié son compte en cliquant sur un lien dans un courrier électronique envoyé à l’utilisateur. Retour {“erreur”: “Compte non vérifié”} ou autre.
  • un utilisateur souhaite commander un livre, mais le livre a été vendu (état modifié dans la firebase database) et ne peut plus être commandé. Retour {“error”: “Livre déjà vendu”}.