Was sollte ein JSON-Dienst bei einem Fehler zurückgeben?

79

Ich schreibe einen JSON-Dienst in C # (.ashx-Datei). Bei erfolgreicher Anforderung an den Dienst gebe ich einige JSON-Daten zurück. Wenn die Anforderung fehlschlägt, entweder weil eine Ausnahme ausgelöst wurde (z. B. Datenbank-Timeout) oder weil die Anforderung in irgendeiner Weise falsch war (z. B. wurde eine ID angegeben, die nicht in der Datenbank vorhanden ist), wie sollte der Dienst reagieren? Welche HTTP-Statuscodes sind sinnvoll und sollte ich gegebenenfalls Daten zurückgeben?

Ich gehe davon aus, dass der Dienst hauptsächlich von jQuery mit dem Plugin jQuery.form aufgerufen wird. Verfügt jQuery oder dieses Plugin über eine Standardmethode zur Behandlung einer Fehlerantwort?

BEARBEITEN: Ich habe beschlossen, bei Erfolg jQuery + .ashx + HTTP [Statuscodes] zu verwenden. Ich gebe JSON zurück, aber bei einem Fehler gebe ich eine Zeichenfolge zurück, da dies anscheinend die Fehleroption für jQuery ist. Ajax erwartet.

thatismatt
quelle

Antworten:

34

Der von Ihnen zurückgegebene HTTP-Statuscode sollte von der Art des aufgetretenen Fehlers abhängen. Wenn in der Datenbank keine ID vorhanden ist, geben Sie eine 404 zurück. Wenn ein Benutzer nicht über genügend Berechtigungen verfügt, um diesen Ajax-Aufruf auszuführen, geben Sie einen 403 zurück. Wenn die Datenbank eine Zeitüberschreitung aufweist, bevor der Datensatz gefunden werden kann, wird eine 500 zurückgegeben (Serverfehler).

jQuery erkennt solche Fehlercodes automatisch und führt die Rückruffunktion aus, die Sie in Ihrem Ajax-Aufruf definiert haben. Dokumentation: http://api.jquery.com/jQuery.ajax/

Kurzes Beispiel für einen $.ajaxFehlerrückruf:

$.ajax({
  type: 'POST',
  url: '/some/resource',
  success: function(data, textStatus) {
    // Handle success
  },
  error: function(xhr, textStatus, errorThrown) {
    // Handle error
  }
});
Ron DeVera
quelle
3
Welchen Fehlercode sollte ich zurückgeben, wenn jemand ungültige Daten liefert, z. B. eine Zeichenfolge, für die eine Ganzzahl erforderlich war? oder eine ungültige E-Mail-Adresse?
Thatismatt
etwas im Bereich von 500, wie jeder ähnliche serverseitige
Codefehler
7
Der Bereich 500 ist ein Serverfehler, aber auf dem Server ist nichts schiefgegangen. Sie haben eine schlechte Anfrage gestellt, sollte es also nicht im Bereich 400 liegen?
Thatismatt
38
Wenn ich als Benutzer eine 500 erhalte, weiß ich, dass ich nicht schuld bin. Wenn ich eine 400 erhalte, kann ich herausfinden, was ich falsch gemacht habe. Dies ist besonders wichtig, wenn ich eine API schreibe, da Ihre Benutzer technisch versiert sind und eine 400 fordert sie auf, die API korrekt zu verwenden. PS - Ich bin damit einverstanden, dass ein DB-Timeout 500 sein sollte.
Thatismatt
4
Ich möchte nur darauf hinweisen, dass 404 bedeutet, dass die adressierte Ressource fehlt. In diesem Fall ist die Ressource Ihr POST-Prozessor, keine zufällige Sache in Ihrer Datenbank mit einer ID. In diesem Fall ist 400 besser geeignet.
StevenC
56

In dieser Frage erhalten Sie einen Einblick in Best Practices für Ihre Situation.

Der Topline-Vorschlag (über diesen Link) besteht darin, eine Antwortstruktur (sowohl für Erfolg als auch für Misserfolg) zu standardisieren, nach der Ihr Handler sucht, alle Ausnahmen auf der Serverebene abzufangen und in dieselbe Struktur zu konvertieren. Zum Beispiel (aus dieser Antwort ):

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

Dies ist der Ansatz, den Stackoverflow verwendet (falls Sie sich gefragt haben, wie andere so etwas tun). Schreibvorgänge wie Abstimmungen haben "Success"und "Message"Felder, unabhängig davon, ob die Abstimmung erlaubt war oder nicht:

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

Wie @ Phil.H betonte , sollten Sie bei jeder Auswahl konsequent sein. Dies ist leichter gesagt als getan (wie alles in der Entwicklung!).

Zum Beispiel, wenn Sie Kommentare zu SO zu schnell einreichen, anstatt konsistent zu sein und zurückzukehren

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

SO löst eine Serverausnahme ( HTTP 500) aus und fängt sie in ihrem errorRückruf ab.

So sehr es sich "richtig anfühlt", jQuery + .ashx+ HTTP [Statuscodes] IMO zu verwenden, wird Ihre clientseitige Codebasis komplexer, als es sich lohnt. Stellen Sie fest, dass jQuery keine Fehlercodes "erkennt", sondern das Fehlen eines Erfolgscodes. Dies ist eine wichtige Unterscheidung, wenn Sie versuchen, einen Client anhand von http-Antwortcodes mit jQuery zu entwerfen. Sie haben nur zwei Möglichkeiten (war es ein "Erfolg" oder ein "Fehler"?), Die Sie selbst weiter verzweigen müssen. Wenn Sie eine kleine Anzahl von WebServices haben, die eine kleine Anzahl von Seiten steuern, ist dies möglicherweise in Ordnung, aber alles, was größer ist, kann unordentlich werden.

In einem .asmxWebService (oder WCF) ist es viel natürlicher , ein benutzerdefiniertes Objekt zurückzugeben, als den HTTP-Statuscode anzupassen. Außerdem erhalten Sie die JSON-Serialisierung kostenlos.

Halbmond frisch
quelle
1
Gültiger Ansatz, nur ein Trottel: Beispiele sind nicht gültig JSON (fehlende doppelte Anführungszeichen für Schlüsselnamen)
StaxMan
1
Das habe ich früher gemacht, aber du solltest wirklich http-Statuscodes verwenden, dafür sind sie da (besonders wenn du RESTful-Sachen machst)
Eva
Ich denke, dieser Ansatz ist definitiv gültig - http-Statuscodes sind nützlich, um erholsame Dinge zu erledigen, aber nicht so hilfreich, wenn Sie beispielsweise API-Aufrufe an ein Skript ausführen, das eine Datenbankabfrage enthält. Selbst wenn die Datenbankabfrage einen Fehler zurückgibt, ist der http-Statuscode immer noch 200. In diesem Fall verwende ich normalerweise den Schlüssel 'Erfolg', um anzuzeigen, ob die MySQL-Abfrage erfolgreich war oder nicht :)
Terry
17

Die Verwendung von HTTP-Statuscodes wäre eine RESTful-Methode, dies würde jedoch bedeuten, dass Sie den Rest der Schnittstelle mithilfe von Ressourcen-URIs usw. RESTful machen.

Definieren Sie die Schnittstelle in Wahrheit so, wie Sie möchten (geben Sie ein Fehlerobjekt zurück, z. B. die Eigenschaft mit dem Fehler detailliert, und einen Teil des HTML-Codes, der dies erklärt, usw.), aber sobald Sie sich für etwas entschieden haben, das in einem Prototyp funktioniert rücksichtslos konsequent sein.

Phil H.
quelle
Ich mag, was Sie vorschlagen, ich gehe davon aus, dass Sie denken, ich sollte JSON dann zurückgeben? So etwas wie: {error: {message: "Ein Fehler ist aufgetreten", Details: "Es ist aufgetreten, weil es Montag ist."}}
thatismatt
@thatismatt - Das ist durchaus vernünftig, wenn Fehler immer schwerwiegend sind. Für mehr Granularität erhalten Sie einiges an Flexibilität, wenn Sie errorein (möglicherweise leeres) Array erstellen und einen fatal_error: boolParameter hinzufügen .
Ben Blank
2
Oh, und +1 für RESTful-Antworten, wann sie verwendet werden sollen und wann nicht. :-)
Ben Blank
Ron DeVera hat erklärt, was ich dachte!
Phil H
3

Ich denke, wenn Sie nur eine Ausnahme auslösen, sollte dies im jQuery-Rückruf behandelt werden, der für die Option 'error' übergeben wird . (Wir protokollieren diese Ausnahme auch auf der Serverseite in einem zentralen Protokoll.) Kein spezieller HTTP-Fehlercode erforderlich, aber ich bin gespannt, was andere Leute auch tun.

Das ist was ich tue, aber das sind nur meine $ .02

Wenn Sie RESTful sein und Fehlercodes zurückgeben möchten, halten Sie sich an die vom W3C festgelegten Standardcodes: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

Dan Esparza
quelle
3

Ich habe einige Stunden damit verbracht, dieses Problem zu lösen. Meine Lösung basiert auf folgenden Wünschen / Anforderungen:

  • In allen JSON-Controller-Aktionen ist kein Code zur wiederholten Behandlung von Boilerplate-Fehlern enthalten.
  • Bewahren Sie die HTTP-Statuscodes (Fehlercodes) auf. Warum? Weil Bedenken auf höherer Ebene die Implementierung auf niedrigerer Ebene nicht beeinflussen sollten.
  • Sie können JSON-Daten abrufen, wenn auf dem Server ein Fehler / eine Ausnahme auftritt. Warum? Weil ich vielleicht reichhaltige Fehlerinformationen haben möchte. ZB Fehlermeldung, domänenspezifischer Fehlerstatuscode, Stack-Trace (in Debug- / Entwicklungsumgebung).
  • Benutzerfreundlichkeit clientseitig - vorzugsweise mit jQuery.

Ich erstelle ein HandleErrorAttribute (Erläuterungen zu den Details finden Sie in den Codekommentaren). Einige Details, einschließlich "Verwendungen", wurden weggelassen, sodass der Code möglicherweise nicht kompiliert werden kann. Ich füge den Filter den globalen Filtern während der Anwendungsinitialisierung in Global.asax.cs folgendermaßen hinzu:

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

Ich habe das folgende jQuery-Plugin für die clientseitige Benutzerfreundlichkeit entwickelt:

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

Was bekomme ich von all dem? Das Endergebnis ist das

  • Keine meiner Controller-Aktionen hat Anforderungen an HandleErrorAttributes.
  • Keine meiner Steuerungsaktionen enthält einen sich wiederholenden Fehlerbehandlungscode für die Kesselplatte.
  • Ich habe einen einzigen Fehlerbehandlungscode, mit dem ich die Protokollierung und andere mit der Fehlerbehandlung verbundene Dinge einfach ändern kann.
  • Eine einfache Anforderung: Controller-Aktionen, die JsonResult zurückgeben, müssen den Rückgabetyp JsonResult und keinen Basistyp wie ActionResult haben. Grund: Siehe Codekommentar in FooHandleErrorAttribute.

Client-seitiges Beispiel:

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

Kommentare sind herzlich willkommen! Ich werde wahrscheinlich eines Tages über diese Lösung bloggen ...

Bjarke
quelle
boooo! Es ist besser, eine einfache Antwort mit nur einer notwendigen Erklärung zu haben, als eine große Antwort, um eine bestimmte Situation zu befriedigen. Gehen Sie das nächste Mal für eine allgemeine Antwort, damit jeder sie verwenden kann
pythonian29033
2

Ich würde definitiv einen 500-Fehler mit einem JSON-Objekt zurückgeben, das die Fehlerbedingung beschreibt, ähnlich wie ein ASP.NET AJAX "ScriptService" -Fehler zurückkehrt . Ich glaube, das ist ziemlich normal. Es ist auf jeden Fall schön, diese Konsistenz bei der Behandlung potenziell unerwarteter Fehlerbedingungen zu haben.

Warum nicht einfach die in .NET integrierte Funktionalität verwenden, wenn Sie sie in C # schreiben? WCF- und ASMX-Dienste machen es einfach, Daten als JSON zu serialisieren, ohne das Rad neu zu erfinden.

Dave Ward
quelle
Ich denke nicht, dass 500 Fehlercode in diesem Zusammenhang verwendet werden sollte. Basierend auf der Spezifikation: w3.org/Protocols/rfc2616/rfc2616-sec10.html ist die beste Alternative das Senden einer 400 (fehlerhafte Anfrage). 500 Fehler ist besser für eine nicht behandelte Ausnahme geeignet.
Gabriel Mazetto
2

Schienengerüste werden 422 Unprocessable Entityfür diese Art von Fehlern verwendet. Weitere Informationen finden Sie in RFC 4918 .

ZiggyTheHamster
quelle
2

Ja, Sie sollten HTTP-Statuscodes verwenden. Und auch vorzugsweise Fehlerbeschreibungen in einem etwas standardisierten JSON-Format zurückgeben, wie Nottinghams Vorschlag , siehe Apigility Error Reporting :

Die Nutzlast eines API-Problems hat folgende Struktur:

  • Typ : Eine URL zu einem Dokument, das den Fehlerzustand beschreibt (optional und "about: blank" wird angenommen, wenn keine angegeben ist; sollte in ein für Menschen lesbares Dokument aufgelöst werden; Apigility stellt dies immer bereit).
  • title : Ein kurzer Titel für die Fehlerbedingung (erforderlich; sollte für jedes Problem des gleichen Typs gleich sein ; Apigility bietet dies immer an).
  • status : Der HTTP-Statuscode für die aktuelle Anforderung (optional; Apigility stellt dies immer bereit).
  • Detail : Fehlerdetails für diese Anforderung (optional; Apigility erfordert dies für jedes Problem).
  • Instanz : URI, die die spezifische Instanz dieses Problems identifiziert (optional; Apigility bietet dies derzeit nicht an).
mb21
quelle
1

Wenn der Benutzer ungültige Daten angibt, sollte dies auf jeden Fall eine sein 400 Bad Request( Die Anforderung enthält eine schlechte Syntax oder kann nicht erfüllt werden. )

Daniel Serodio
quelle
JEDER der 400er Bereiche ist akzeptabel und 422 ist die beste Option für Daten, die nicht verarbeitet werden können
Jamesc
0

Ich denke nicht, dass Sie irgendwelche http-Fehlercodes zurückgeben sollten, sondern benutzerdefinierte Ausnahmen, die für das Client-Ende der Anwendung nützlich sind, damit die Schnittstelle weiß, was tatsächlich passiert ist. Ich würde nicht versuchen, echte Probleme mit 404-Fehlercodes oder etwas Ähnlichem zu maskieren.

Quintin Robinson
quelle
Schlagen Sie vor, ich gebe eine 200 zurück, auch wenn etwas schief geht? Was meinst du mit "benutzerdefinierte Ausnahme"? Meinen Sie ein Stück JSON, das den Fehler beschreibt?
Thatismatt
4
Blah, das Zurückgeben von http-Code bedeutet nicht, dass Sie AUCH keine Fehlerbeschreibungsnachricht zurückgeben konnten. 200 zurückzugeben wäre ziemlich dumm, ganz zu schweigen von falsch.
StaxMan
Mit @StaxMan einverstanden - immer den besten Statuscode zurückgeben, aber die Beschreibung in die Rückgabeinformationen aufnehmen
schmoopy
0

Bei Server- / Protokollfehlern würde ich versuchen, so REST / HTTP wie möglich zu sein (Vergleichen Sie dies mit der Eingabe von URLs in Ihrem Browser):

  • Ein nicht vorhandenes Element wird aufgerufen (/ personen / {nicht vorhandene ID hier)). Geben Sie eine 404 zurück.
  • Auf dem Server ist ein unerwarteter Fehler (Codefehler) aufgetreten. Geben Sie eine 500 zurück.
  • Der Client-Benutzer ist nicht berechtigt, die Ressource abzurufen. Geben Sie einen 401 zurück.

Für domänen- / geschäftslogikspezifische Fehler würde ich sagen, dass das Protokoll richtig verwendet wird und kein serverinterner Fehler vorliegt. Antworten Sie daher mit einem Fehler-JSON / XML-Objekt oder einem beliebigen Fehler, mit dem Sie Ihre Daten beschreiben möchten (Vergleichen Sie dies mit dem Ausfüllen Formulare auf einer Website):

  • Ein Benutzer möchte seinen Kontonamen ändern, aber der Benutzer hat sein Konto noch nicht überprüft, indem er auf einen Link in einer E-Mail geklickt hat, die an den Benutzer gesendet wurde. Rückgabe {"Fehler": "Konto nicht verifiziert"} oder was auch immer.
  • Ein Benutzer möchte ein Buch bestellen, aber das Buch wurde verkauft (Status in DB geändert) und kann nicht mehr bestellt werden. Rückgabe {"Fehler": "Buch bereits verkauft"}.
Almer
quelle