¿Qué debería devolver un servicio JSON en caso de falla / error?

79

Estoy escribiendo un servicio JSON en C # (archivo .ashx). En una solicitud exitosa al servicio, devuelvo algunos datos JSON. Si la solicitud falla, ya sea porque se lanzó una excepción (p. Ej., Tiempo de espera de la base de datos) o porque la solicitud fue incorrecta de alguna manera (p. Ej., Se proporcionó como argumento una ID que no existe en la base de datos), ¿cómo debería responder el servicio? ¿Qué códigos de estado HTTP son sensibles y debo devolver algún dato, si corresponde?

Anticipo que el servicio se llamará principalmente desde jQuery usando el complemento jQuery.form, ¿jQuery o este complemento tienen alguna forma predeterminada de manejar una respuesta de error?

EDITAR: Decidí que usaré jQuery + .ashx + HTTP [códigos de estado] en caso de éxito, devolveré JSON pero en caso de error, devolveré una cadena, ya que parece que esa es la opción de error para jQuery. espera ajax.

thatismatt
fuente

Respuestas:

34

El código de estado HTTP que devuelva debe depender del tipo de error que se haya producido. Si no existe un ID en la base de datos, devuelva un 404; si un usuario no tiene suficientes privilegios para realizar esa llamada Ajax, devuelve un 403; si la base de datos se agota antes de poder encontrar el registro, devuelve un 500 (error del servidor).

jQuery detecta automáticamente dichos códigos de error y ejecuta la función de devolución de llamada que define en su llamada Ajax. Documentación: http://api.jquery.com/jQuery.ajax/

Ejemplo breve de $.ajaxdevolución de llamada de error:

$.ajax({
  type: 'POST',
  url: '/some/resource',
  success: function(data, textStatus) {
    // Handle success
  },
  error: function(xhr, textStatus, errorThrown) {
    // Handle error
  }
});
Ron DeVera
fuente
3
¿Qué código de error crees que debería devolver si alguien proporciona datos no válidos, como una cadena donde se requiere un número entero? o una dirección de correo electrónico no válida?
thatismatt
algo en el rango 500, igual que cualquier error de código del lado del servidor similar
annakata
7
El rango 500 es un error del servidor, pero nada ha salido mal en el servidor. Hicieron una mala solicitud, ¿no debería estar en el rango de 400?
thatismatt
38
Como usuario, si recibo un 500, sé que no tengo la culpa, si recibo un 400 puedo averiguar qué hice mal, esto es particularmente importante al escribir una API, ya que sus usuarios son técnicamente alfabetizados y un 400 les dice que usen la API correctamente. PD: estoy de acuerdo en que un tiempo de espera de DB debe ser 500.
Eso es
4
Solo quiero señalar que 404 significa que falta el recurso direccionado . En este caso, el recurso es su procesador POST, no algo aleatorio en su base de datos con una identificación. En este caso 400 es más apropiado.
StevenC
56

Consulte esta pregunta para obtener información sobre las mejores prácticas para su situación.

La sugerencia principal (de dicho enlace) es estandarizar una estructura de respuesta (tanto para el éxito como para el fracaso) que su controlador busca, capturando todas las Excepciones en la capa del servidor y convirtiéndolas a la misma estructura. Por ejemplo (de esta respuesta ):

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

Este es el enfoque que usa stackoverflow (en caso de que se pregunte cómo hacen los demás este tipo de cosas); escribir operaciones como votar have "Success"y "Message"fields, independientemente de si se permitió el voto o no:

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

Como señaló @ Phil.H , debes ser consistente en lo que elijas. Es más fácil decirlo que hacerlo (¡como todo lo que está en desarrollo!).

Por ejemplo, si envía comentarios demasiado rápido sobre SO, en lugar de ser coherente y regresar

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

SO lanzará una excepción de servidor ( HTTP 500) y la detectará en su errordevolución de llamada.

Por mucho que "se sienta bien" usar jQuery + .ashx+ HTTP [códigos de estado] IMO, agregará más complejidad a la base de código del lado del cliente de lo que vale. Tenga en cuenta que jQuery no "detecta" códigos de error, sino más bien la falta de un código de éxito. Esta es una distinción importante cuando se intenta diseñar un cliente en torno a códigos de respuesta http con jQuery. Solo tiene dos opciones (¿fue un "éxito" o un "error"?), Que debe ampliar por su cuenta. Si tiene una pequeña cantidad de WebServices que manejan una pequeña cantidad de páginas, entonces podría estar bien, pero cualquier escala mayor puede complicarse.

Es mucho más natural en un .asmxWebService (o WCF para el caso) devolver un objeto personalizado que personalizar el código de estado HTTP. Además, obtiene la serialización JSON de forma gratuita.

Creciente fresco
fuente
1
Enfoque válido, solo un detalle: los ejemplos no son JSON válidos (faltan comillas dobles para los nombres de clave)
StaxMan
1
esto es lo que solía hacer, pero realmente deberías usar códigos de estado http, para eso están ahí (especialmente si estás haciendo cosas RESTful)
Eva
Creo que este enfoque es definitivamente válido: los códigos de estado http son útiles para hacer cosas tranquilas, pero no tan útiles cuando, por ejemplo, estás haciendo llamadas api a un script que alberga una consulta de base de datos. Incluso cuando la consulta de la base de datos devuelve un error, el código de estado http seguirá siendo 200. En este caso, normalmente uso la tecla "éxito" para indicar si la consulta de MySQL ha tenido éxito o no :)
Terry
17

Usar códigos de estado HTTP sería una forma RESTful de hacerlo, pero eso sugeriría que haga que el resto de la interfaz sea RESTful usando URI de recursos y así sucesivamente.

En realidad, defina la interfaz como desee (devuelva un objeto de error, por ejemplo, detallando la propiedad con el error y un fragmento de HTML que lo explique, etc.), pero una vez que haya decidido algo que funcione en un prototipo , sea implacablemente coherente.

Phil H
fuente
Me gusta lo que está sugiriendo, ¿supongo que cree que debería devolver JSON entonces? Algo como: {error: {mensaje: "Ocurrió un error", detalles: "Ocurrió porque es lunes"}}
eso es
@thatismatt - Eso es bastante razonable, si los errores siempre son fatales. Para mayor granularidad, hacer erroruna matriz (posiblemente vacía) y agregar un fatal_error: boolparámetro le dará bastante flexibilidad.
Ben Blank
2
Ah, y +1 para respuestas RESTful sobre cuándo usar y cuándo no usar. :-)
Ben Blank
¡Ron DeVera me ha explicado lo que estaba pensando!
Phil H
3

Creo que si solo publica una excepción, debe manejarse en la devolución de llamada de jQuery que se pasa para la opción 'error' . (También registramos esta excepción en el lado del servidor en un registro central). No se requiere un código de error HTTP especial, pero tengo curiosidad por ver qué hacen otras personas también.

Esto es lo que hago, pero eso es solo mi $ .02

Si va a estar RESTful y devolver códigos de error, intente ceñirse a los códigos estándar establecidos por el W3C: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

Dan Esparza
fuente
3

He pasado algunas horas resolviendo este problema. Mi solución se basa en los siguientes deseos / requisitos:

  • No tenga un código repetitivo de manejo de errores repetitivo en todas las acciones del controlador JSON.
  • Conservar los códigos de estado HTTP (error). ¿Por qué? Porque las preocupaciones de nivel superior no deberían afectar la implementación de nivel inferior.
  • Poder obtener datos JSON cuando ocurra un error / excepción en el servidor. ¿Por qué? Porque es posible que desee información detallada sobre errores. Por ejemplo, mensaje de error, código de estado de error específico del dominio, seguimiento de pila (en entorno de depuración / desarrollo).
  • Facilidad de uso del lado del cliente: es preferible utilizar jQuery.

Creo un HandleErrorAttribute (consulte los comentarios del código para obtener una explicación de los detalles). Se han omitido algunos detalles, incluidos los "usos", por lo que es posible que el código no se compile. Agrego el filtro a los filtros globales durante la inicialización de la aplicación en Global.asax.cs así:

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

Atributo:

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

Desarrollé el siguiente complemento jQuery para facilitar el uso del lado del cliente:

(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é obtengo de todo esto? El resultado final es que

  • Ninguna de las acciones de mi controlador tiene requisitos en HandleErrorAttributes.
  • Ninguna de las acciones de mi controlador contiene un código repetitivo de manejo de errores de la placa de la caldera.
  • Tengo un solo punto de código de manejo de errores que me permite cambiar fácilmente el registro y otras cosas relacionadas con el manejo de errores.
  • Un requisito simple: las acciones del controlador que devuelven JsonResult deben tener el tipo de retorno JsonResult y no algún tipo base como ActionResult. Razón: Ver comentario de código en FooHandleErrorAttribute.

Ejemplo del lado del cliente:

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

¡Los comentarios son bienvenidos! Probablemente algún día escribiré en un blog sobre esta solución ...

Bjarke
fuente
boooo! es mejor tener una respuesta simple con solo una explicación necesaria que una gran respuesta para satisfacer una situación específica.
busque
2

Definitivamente devolvería un error 500 con un objeto JSON que describe la condición de error, similar a cómo regresa un error ASP.NET AJAX "ScriptService" . Creo que esto es bastante estándar. Definitivamente es bueno tener esa consistencia al manejar condiciones de error potencialmente inesperadas.

Aparte, ¿por qué no usar la funcionalidad incorporada en .NET, si la está escribiendo en C #? Los servicios WCF y ASMX facilitan la serialización de datos como JSON, sin reinventar la rueda.

Dave Ward
fuente
No creo que el código de error 500 deba usarse en este contexto. Según la especificación: w3.org/Protocols/rfc2616/rfc2616-sec10.html , la mejor alternativa es enviar un 400 (solicitud incorrecta). El error 500 es más adecuado para una excepción no controlada.
Gabriel Mazetto
2

Los andamios de rieles se utilizan 422 Unprocessable Entitypara este tipo de errores. Consulte RFC 4918 para obtener más información.

ZiggyTheHamster
fuente
2

Sí, debe utilizar códigos de estado HTTP. Y también, preferiblemente, devolver descripciones de errores en un formato JSON algo estandarizado, como la propuesta de Nottingham , consulte un Informe de errores de habilidad :

La carga útil de un problema de API tiene la siguiente estructura:

  • type : una URL a un documento que describe la condición de error (opcional, y se asume "about: blank" si no se proporciona ninguno; debería resolverse en un documento legible por humanos ; Apigility siempre proporciona esto).
  • title : un título breve para la condición de error (obligatorio; y debería ser el mismo para todos los problemas del mismo tipo ; Apigility siempre lo proporciona).
  • status : el código de estado HTTP para la solicitud actual (opcional; Apigility siempre lo proporciona).
  • detalle : detalles de error específicos de esta solicitud (opcional; Apigility lo requiere para cada problema).
  • instancia : URI que identifica la instancia específica de este problema (opcional; Apigility actualmente no proporciona esto).
mb21
fuente
1

Si el usuario proporciona datos no válidos, definitivamente debería ser un 400 Bad Request( La solicitud contiene una sintaxis incorrecta o no se puede cumplir ) .

Daniel Serodio
fuente
CUALQUIERA del rango 400 es aceptable y 422 es la mejor opción para los datos que no se pueden procesar
jamesc
0

No creo que deba devolver ningún código de error http, sino excepciones personalizadas que son útiles para el cliente final de la aplicación para que la interfaz sepa lo que realmente ocurrió. No trataría de ocultar problemas reales con códigos de error 404 o algo por el estilo.

Quintin Robinson
fuente
¿Estás sugiriendo que devuelva un 200 incluso si algo sale mal? ¿Qué quiere decir "excepción personalizada"? ¿Te refieres a un fragmento de JSON que describe el error?
thatismatt
4
Blah, devolver el código http no significa que TAMBIÉN pueda devolver el mensaje de descripción del error. Devolver 200 sería bastante tonto por no mencionar que está mal.
StaxMan
De acuerdo con @StaxMan: siempre devuelva el mejor código de estado pero incluya la descripción en la información de devolución
schmoopy
0

Para errores de servidor / protocolo, trataría de ser lo más REST / HTTP posible (Compare esto con usted escribiendo URL en su navegador):

  • un elemento no existente se llama (/ persons / {non-existing-id-here}). Devuelve un 404.
  • Ocurrió un error inesperado en el servidor (error de código). Devuelve 500.
  • el usuario del cliente no está autorizado para obtener el recurso. Devuelve un 401.

Para errores específicos de dominio / lógica comercial, diría que el protocolo se usa de la manera correcta y no hay un error interno del servidor, así que responda con un objeto JSON / XML de error o lo que prefiera para describir sus datos (Compare esto con usted completando formularios en un sitio web):

  • un usuario desea cambiar el nombre de su cuenta, pero el usuario aún no verificó su cuenta haciendo clic en un enlace en un correo electrónico que se envió al usuario. Devuelve {"error": "Cuenta no verificada"} o lo que sea.
  • un usuario quiere pedir un libro, pero el libro se vendió (cambio de estado en DB) y ya no se puede pedir. Devolver {"error": "Libro ya vendido"}.
Almer
fuente