Co powinna zwracać usługa JSON w przypadku niepowodzenia / błędu


79

Piszę usługę JSON w C # (plik .ashx). Po pomyślnym zgłoszeniu do serwisu zwracam dane JSON. Jeśli żądanie nie powiedzie się, albo z powodu zgłoszenia wyjątku (np. Przekroczenia limitu czasu bazy danych), albo z powodu jakiegoś błędnego żądania (np. Jako argument podano identyfikator, którego nie ma w bazie danych), jak powinna odpowiedzieć usługa? Jakie kody stanu HTTP są rozsądne i czy powinienem zwrócić jakiekolwiek dane, jeśli takie istnieją?

Przewiduję, że usługa będzie wywoływana głównie z jQuery za pomocą wtyczki jQuery.form, czy jQuery lub ta wtyczka mają domyślny sposób obsługi odpowiedzi na błąd?

EDYCJA: Zdecydowałem, że użyję jQuery + .ashx + HTTP [kody stanu] w przypadku sukcesu zwrócę JSON, ale w przypadku błędu zwrócę ciąg, ponieważ wydaje się, że to jest opcja błędu dla jQuery. Ajax oczekuje.

Odpowiedzi:


34

Zwracany kod stanu HTTP powinien zależeć od typu błędu, który wystąpił. Jeśli identyfikator nie istnieje w bazie danych, zwróć 404; jeśli użytkownik nie ma wystarczających uprawnień, aby wykonać to wywołanie Ajax, zwraca 403; jeśli baza danych przekroczy limit czasu, zanim będzie mogła znaleźć rekord, zwraca 500 (błąd serwera).

jQuery automatycznie wykrywa takie kody błędów i uruchamia funkcję zwrotną zdefiniowaną w wywołaniu Ajax. Dokumentacja: http://api.jquery.com/jQuery.ajax/

Krótki przykład $.ajaxwywołania zwrotnego błędu:

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

3
Jak myślisz, jaki kod błędu powinienem zwrócić, jeśli ktoś poda nieprawidłowe dane, takie jak ciąg, w którym wymagana była liczba całkowita? lub nieprawidłowy adres e-mail?
thatismatt

coś z zakresu 500, tak samo jak każdy podobny błąd kodu po stronie serwera
annakata

7
Zakres 500 to błąd serwera, ale nic nie poszło źle na serwerze. Złożyli złą prośbę, więc czy nie powinno być w zakresie 400?
thatismatt

38
Jako użytkownik, jeśli otrzymam 500, wiem, że nie jestem winny, jeśli otrzymam 400, mogę ustalić, co zrobiłem źle, jest to szczególnie ważne przy pisaniu API, ponieważ twoi użytkownicy są technicznie piśmienni i 400 mówi im, aby poprawnie używali API. PS - Zgadzam się, że limit czasu DB powinien wynosić 500.
thatismatt

4
Chcę tylko zaznaczyć, że 404 oznacza brak adresowanego zasobu . W tym przypadku zasobem jest twój procesor POST, a nie jakaś przypadkowa rzecz w twojej bazie danych z identyfikatorem. W tym przypadku 400 jest bardziej odpowiednie.
StevenC

56

Zapoznaj się z tym pytaniem, aby uzyskać wgląd w najlepsze praktyki w Twojej sytuacji.

Największą sugestią (ze wspomnianego linku) jest ustandaryzowanie struktury odpowiedzi (zarówno pod kątem sukcesu, jak i porażki), której szuka twój program obsługi, przechwytując wszystkie wyjątki w warstwie serwera i konwertując je do tej samej struktury. Na przykład (z tej odpowiedzi ):

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

To jest podejście, które stosuje stackoverflow (na wypadek, gdybyś się zastanawiał, jak inni robią takie rzeczy); operacje zapisu, takie jak głosowanie mają "Success"i "Message"pola, niezależnie od tego, czy głosowanie było dozwolone, czy nie:

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

Jak zauważył @ Phil.H , powinieneś być konsekwentny we wszystkim, co wybierzesz. Łatwiej to powiedzieć niż zrobić (jak wszystko w trakcie opracowywania!).

Na przykład, jeśli zbyt szybko przesyłasz komentarze na temat SO, zamiast być konsekwentnym i zwracać

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

SO zgłosi wyjątek serwera (HTTP 500 ) i przechwyci go w swoim errorwywołaniu zwrotnym.

O ile "wydaje się właściwe" używanie jQuery + .ashx+ HTTP [kody stanu] IMO, zwiększy to złożoność bazy kodu po stronie klienta, niż jest to warte. Zrozum, że jQuery nie „wykrywa” kodów błędów, ale raczej brak kodu sukcesu. Jest to ważna różnica, gdy próbuje się zaprojektować klienta w oparciu o kody odpowiedzi http za pomocą jQuery. Masz tylko dwie możliwości (czy był to „sukces” czy „błąd”?), Które musisz samodzielnie rozgałęzić. Jeśli masz niewielką liczbę usług sieciowych obsługujących niewielką liczbę stron, może to być w porządku, ale wszystko na większą skalę może się skomplikować.

W .asmxusłudze sieci Web (lub w tym przypadku WCF) o wiele bardziej naturalne jest zwracanie obiektu niestandardowego niż dostosowywanie kodu stanu HTTP. Dodatkowo serializacja JSON jest bezpłatna.


1
Prawidłowe podejście, tylko jeden chwytak: przykłady nie są poprawnymi
kodami

1
to właśnie robiłem, ale naprawdę powinieneś używać kodów statusu http, do tego służą (szczególnie jeśli robisz rzeczy RESTful)
Eva

Myślę, że to podejście jest zdecydowanie poprawne - kody statusu http są przydatne do robienia spokojnych rzeczy, ale nie są tak pomocne, gdy, powiedzmy, wykonujesz wywołania interfejsu API do skryptu, który przechowuje zapytanie do bazy danych. Nawet jeśli zapytanie do bazy danych zwróci błąd, kod statusu http będzie nadal wynosić 200. W tym przypadku zazwyczaj używam klucza „sukces”, aby wskazać, czy zapytanie MySQL zakończyło się pomyślnie, czy nie :)
Terry,

17

Używanie kodów stanu HTTP byłoby sposobem na wykonanie tego w trybie REST, ale sugerowałoby to, aby reszta interfejsu była zgodna z REST przy użyciu identyfikatorów URI zasobów i tak dalej.

Prawdę mówiąc, zdefiniuj interfejs tak, jak chcesz (zwróć obiekt błędu, na przykład, wyszczególniając właściwość z błędem i fragment kodu HTML, który go wyjaśnia itp.), Ale kiedy już zdecydujesz się na coś, co działa w prototypie , bądź bezwzględnie konsekwentny.


Podoba mi się to, co sugerujesz, zakładam, że myślisz, że powinienem wtedy zwrócić JSON? Coś w rodzaju: {błąd: {komunikat: „Wystąpił błąd”, szczegóły: „Wystąpił, ponieważ jest poniedziałek.”}}
thatismatt

@thatismatt - To całkiem rozsądne, jeśli błędy są zawsze fatalne. Aby uzyskać większą szczegółowość, utworzenie error(prawdopodobnie pustej) tablicy i dodanie fatal_error: boolparametru zapewni sporą elastyczność.
Ben Blank,

2
Aha, i +1 dla odpowiedzi RESTful kiedy używać i kiedy nie używać. :-)
Ben Blank

Ron DeVera wyjaśnił, o czym myślę!
Phil H.

3

Myślę, że jeśli po prostu zaznaczasz wyjątek, powinien być obsługiwany w wywołaniu zwrotnym jQuery, które jest przekazywane dla opcji „błąd” . (Rejestrujemy również ten wyjątek po stronie serwera w dzienniku centralnym). Nie jest wymagany żaden specjalny kod błędu HTTP, ale jestem ciekawy, co robią też inni ludzie.

To właśnie robię, ale to tylko 0,02 dolara

Jeśli zamierzasz być RESTful i zwracać kody błędów, spróbuj trzymać się standardowych kodów określonych przez W3C: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html


3

Spędziłem kilka godzin, rozwiązując ten problem. Moje rozwiązanie opiera się na następujących życzeniach / wymaganiach:

  • Nie używaj powtarzającego się standardowego kodu obsługi błędów we wszystkich akcjach kontrolera JSON.
  • Zachowaj kody stanu HTTP (błąd). Czemu? Ponieważ problemy wyższego poziomu nie powinny wpływać na implementację niższego poziomu.
  • Być w stanie uzyskać dane JSON, gdy na serwerze wystąpi błąd / wyjątek. Czemu? Ponieważ chciałbym uzyskać szczegółowe informacje o błędach. Np. Komunikat o błędzie, kod statusu błędu specyficzny dla domeny, ślad stosu (w środowisku debugowania / programowania).
  • Łatwość obsługi po stronie klienta - najlepiej przy użyciu jQuery.

Tworzę HandleErrorAttribute (zobacz komentarze do kodu, aby uzyskać wyjaśnienie szczegółów). Kilka szczegółów, w tym „użycie”, zostało pominiętych, więc kod może się nie skompilować. Dodaję filtr do filtrów globalnych podczas inicjalizacji aplikacji w Global.asax.cs w ten sposób:

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

Atrybut:

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

Opracowałem następującą wtyczkę jQuery, aby ułatwić obsługę po stronie klienta:

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

Co mam z tego wszystkiego? Ostateczny wynik jest taki

  • Żadna z moich akcji kontrolera nie ma wymagań dotyczących HandleErrorAttributes.
  • Żadne z moich działań kontrolera nie zawiera powtarzającego się kodu obsługi błędów płyty kotłowej.
  • Mam pojedynczy punkt kodu obsługi błędów, który pozwala mi łatwo zmieniać rejestrowanie i inne rzeczy związane z obsługą błędów.
  • Prosty wymóg: akcje kontrolera zwracające JsonResult muszą mieć zwracany typ JsonResult, a nie jakiś typ podstawowy, taki jak ActionResult. Przyczyna: zobacz komentarz do kodu w FooHandleErrorAttribute.

Przykład po stronie klienta:

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

Komentarze są mile widziane! Pewnie kiedyś napiszę o tym rozwiązaniu ...


boooo! lepiej mieć prostą odpowiedź zawierającą tylko niezbędne wyjaśnienie niż obszerną odpowiedź w celu zaspokojenia określonej sytuacji. następnym razem wybierz ogólną odpowiedź, aby każdy mógł z niej skorzystać
pythonian29033

2

Zdecydowanie zwróciłbym błąd 500 z obiektem JSON opisującym stan błędu, podobnie jak w przypadku zwracania błędu ASP.NET AJAX „ScriptService” . Uważam, że jest to dość standardowe. Zdecydowanie fajnie jest mieć taką spójność podczas obsługi potencjalnie nieoczekiwanych warunków błędu.

Poza tym, dlaczego nie skorzystać po prostu z wbudowanej funkcjonalności w .NET, jeśli piszesz w C #? Usługi WCF i ASMX ułatwiają serializację danych w formacie JSON bez konieczności ponownego odkrywania koła.


Nie sądzę, aby w tym kontekście używać kodu błędu 500. Bazując na specyfikacji: w3.org/Protocols/rfc2616/rfc2616-sec10.html , najlepszą alternatywą jest wysłanie 400 (błędne żądanie). Błąd 500 jest bardziej odpowiedni dla nieobsługiwanego wyjątku.
Gabriel Mazetto


2

Tak, powinieneś używać kodów stanu HTTP. A także najlepiej zwracać opisy błędów w nieco ustandaryzowanym formacie JSON, takim jak propozycja Nottingham , zobacz raportowanie błędów apigility :

Ładunek problemu API ma następującą strukturę:

  • typ : adres URL do dokumentu opisującego stan błędu (opcjonalny i „about: blank” jest zakładane, jeśli nie zostanie podany; powinien prowadzić do dokumentu czytelnego dla człowieka ; Apigility zawsze to zapewnia).
  • title : krótki tytuł warunku błędu (wymagany; i powinien być taki sam dla każdego problemu tego samego typu ; Apigility zawsze to zapewnia).
  • status : kod statusu HTTP dla bieżącego żądania (opcjonalnie; Apigility zawsze to zapewnia).
  • szczegóły : szczegóły błędu specyficzne dla tego żądania (opcjonalne; Apigility wymaga tego dla każdego problemu).
  • instancja : identyfikator URI identyfikujący konkretną instancję tego problemu (opcjonalnie; Apigility obecnie tego nie zapewnia).

1

Jeśli użytkownik poda nieprawidłowe dane, z pewnością powinno to być 400 Bad Request( Żądanie zawiera złą składnię lub nie może zostać spełnione ).


Jakikolwiek zakres 400 jest dopuszczalne, a 422 jest najlepszym rozwiązaniem dla danych, które nie mogą być przetwarzane
jamesc

0

Nie sądzę, abyś zwracał jakiekolwiek kody błędów http, raczej niestandardowe wyjątki, które są przydatne dla klienta po stronie aplikacji, aby interfejs wiedział, co faktycznie się wydarzyło. Nie próbowałbym maskować prawdziwych problemów z kodami błędów 404 lub czymś podobnym.


Sugerujesz, żebym zwrócił 200, nawet jeśli coś pójdzie nie tak? Co masz na myśli „niestandardowy wyjątek”? Czy masz na myśli kawałek JSON opisujący błąd?
thatismatt

4
Blah, zwrócenie kodu http nie oznacza, że ​​nie można TAKŻE zwrócić komunikatu z opisem błędu. Zwrócenie 200 byłoby raczej głupie, nie wspominając o błędzie.
StaxMan

Uzgodniono z @StaxMan - zawsze zwracaj najlepszy kod statusu, ale
dołącz

0

W przypadku błędów serwera / protokołu starałbym się być jak najbardziej REST / HTTP (porównaj to z wpisywaniem adresów URL w przeglądarce):

  • wywoływana jest nieistniejąca pozycja (/ people / {non-existing-id-here}). Zwróć 404.
  • Wystąpił nieoczekiwany błąd na serwerze (błąd w kodzie). Zwróć 500.
  • użytkownik klienta nie jest upoważniony do pobrania zasobu. Zwróć 401.

W przypadku błędów specyficznych dla domeny / logiki biznesowej powiedziałbym, że protokół jest używany we właściwy sposób i nie ma wewnętrznego błędu serwera, więc odpowiedz błędem obiekt JSON / XML lub cokolwiek wolisz opisać swoje dane (porównaj to z wypełnieniem formularze na stronie internetowej):

  • użytkownik chce zmienić nazwę swojego konta, ale nie zweryfikował jeszcze swojego konta, klikając łącze w wiadomości e-mail wysłanej do użytkownika. Zwróć {"error": "Konto niezweryfikowane"} lub cokolwiek innego.
  • użytkownik chce zamówić książkę, ale książka została sprzedana (stan zmienił się w DB) i nie można jej już zamówić. Zwróć {"error": "Książka już sprzedana"}.
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.