2009-03-23 5 views
71

Sto scrivendo un servizio JSON in C# (file .ashx). In caso di una richiesta riuscita al servizio, restituisco alcuni dati JSON. Se la richiesta non riesce, o perché è stata generata un'eccezione (ad es. Timeout del database) o perché la richiesta era errata in qualche modo (ad esempio, un ID che non esiste nel database è stato fornito come argomento) come dovrebbe rispondere il servizio? Quali codici di stato HTTP sono sensibili e dovrei restituire eventuali dati, se presenti?Cosa deve restituire un servizio JSON in caso di errore/errore

Sto anticipando che il servizio verrà principalmente chiamato da jQuery utilizzando il plug-in jQuery.form, jQuery o questo plugin hanno un modo predefinito di gestire una risposta di errore?

EDIT: Ho deciso io uso jQuery + Ashx + HTTP [codici di stato] in caso di successo tornerò JSON, ma in caso di errore Tornerò una stringa, come sembra che questo è ciò che l'opzione di errore per jQuery.ajax si aspetta.

risposta

31

Il codice di stato HTTP restituito deve dipendere dal tipo di errore verificatosi. Se un ID non esiste nel database, restituisce un 404; se un utente non ha privilegi sufficienti per effettuare quella chiamata Ajax, restituisce un 403; se il database è scaduto prima di riuscire a trovare il record, restituire un errore di 500 (errore del server).

jQuery rileva automaticamente tali codici di errore ed esegue la funzione di richiamata definita nella chiamata Ajax. Documentazione: http://api.jquery.com/jQuery.ajax/

Breve esempio di un callback $.ajax errore:

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

Quale codice di errore pensi che dovrei restituire se qualcuno fornisce dati non validi, come una stringa in cui era richiesto un intero? o un indirizzo email non valido? – thatismatt

+0

qualcosa nell'intervallo 500, uguale a qualsiasi errore del codice lato server simile – annakata

+7

L'intervallo 500 è un errore del server, ma sul server non è andato a buon fine. Hanno fatto una cattiva richiesta, quindi non dovrebbe essere nella gamma 400? – thatismatt

15

L'utilizzo dei codici di stato HTTP sarebbe un modo RESTful per farlo, ma ciò suggerirebbe di rendere il resto dell'interfaccia RESTful utilizzando gli URI delle risorse e così via.

In verità, definire l'interfaccia come desiderato (restituire un oggetto di errore, ad esempio, specificando la proprietà con l'errore e una porzione di HTML che lo spiega, ecc.), Ma una volta deciso qualcosa che funziona in un prototipo, essere spietatamente coerente.

+0

mi piace quello che lei suggerisce, sto supponendo che tu pensi che io tornassi JSON, allora? Qualcosa come: {errore: {messaggio: "Si è verificato un errore", dettagli: "Si è verificato perché è lunedì."}} – thatismatt

+0

@thatismatt: è abbastanza ragionevole, se gli errori sono sempre fatali. Per maggiore granularità, rendere 'error' una matrice (possibilmente vuota) e aggiungere un parametro' fatal_error: bool' ti darà un po 'di flessibilità. –

+1

Oh e +1 per le risposte RESTful quando e quando non lo si usano. :-) –

2

Penso che se bolla un'eccezione, dovrebbe essere gestita nello . (Registriamo anche questa eccezione sul lato server in un registro centrale). Non è richiesto alcun codice di errore HTTP speciale, ma sono curioso di vedere cosa fanno anche gli altri.

Questo è quello che faccio, ma questo è solo il mio $ .02

Se avete intenzione di essere riposante e restituire i codici di errore, cerca di attenersi ai codici standard previsti dal W3C: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

0

Non penso che dovresti restituire codici di errore http, piuttosto eccezioni personalizzate che sono utili al client dell'applicazione, in modo che l'interfaccia sappia cosa si è effettivamente verificato. Non proverei a mascherare problemi reali con i codici di errore 404 o qualcosa del genere.

+0

Stai suggerendo di restituire un 200 anche se qualcosa va storto? Cosa intendi per "eccezione personalizzata"? Intendi un pezzo di JSON che descrive l'errore? – thatismatt

+4

Blah, restituendo il codice http non significa che non si possa ANCHE restituire il messaggio di descrizione dell'errore. Restituire 200 sarebbe piuttosto sciocco, per non dire sbagliato. – StaxMan

+0

Concordato con @StaxMan - restituisce sempre il miglior codice di stato ma include la descrizione nelle informazioni di ritorno – schmoopy

52

Vedi this question per qualche informazione sulle migliori pratiche per la vostra situazione.

Il suggerimento topline (da detto collegamento) serve per standardizzare una struttura di risposta (sia per il successo che per il fallimento) che il gestore cerca, catturando tutte le eccezioni sul livello server e convertendole nella stessa struttura.Per esempio (da this answer):

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

Questo è utilizza l'approccio StackOverflow (nel caso ve lo stiate chiedendo come gli altri fanno questo genere di cose); le operazioni di scrittura come di voto hanno "Success" e "Message" campi, a prescindere se il voto è stato consentito o no:

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

Come @Phil.H pointed out, si dovrebbe essere coerente in quello che vuoi. Questo è più facile a dirsi che a farsi (come è tutto nello sviluppo!).

Per esempio, se si invia commenti troppo in fretta su SO, invece di essere coerenti e di ritorno

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

così sarà un'eccezione del server (HTTP 500) e lo prende nella loro error callback.

Tanto più che "sembra giusto" utilizzare jQuery + .ashx + HTTP [codici di stato] IMO aggiungerà più complessità alla base di codice lato client di quanto valga. Renditi conto che jQuery non "rileva" i codici di errore ma piuttosto la mancanza di un codice di successo. Questa è una distinzione importante quando si cerca di progettare un client intorno ai codici di risposta http con jQuery. Hai solo due scelte (era un "successo" o "errore"?), Che devi ramificare ulteriormente da solo. Se hai un piccolo numero di WebService che guidano un numero limitato di pagine, allora potrebbe andar bene, ma qualsiasi cosa su scala più grande potrebbe diventare disordinata.

È molto più naturale in un servizio Web .asmx (o WCF del caso) restituire un oggetto personalizzato piuttosto che personalizzare il codice di stato HTTP. Inoltre ottieni la serializzazione JSON gratuitamente.

+1

Approccio valido, un solo nitpick: gli esempi non sono JSON validi (mancano virgolette doppie per i nomi delle chiavi) – StaxMan

+1

questo è quello che facevo, ma dovresti usare i codici di stato http, ecco perché sono lì (specialmente se stai facendo cose RESTful) – Eva

+0

Penso che questo approccio sia assolutamente valido - i codici di stato http sono utili per fare cose tranquille, ma non così utili quando, per esempio, stai facendo chiamate API a uno script che ospita una query di database. Anche quando la query del database restituisce un errore, il codice di stato http sarà ancora 200. In questo caso, in genere utilizzo la chiave "success" per indicare se la query MySQL ha avuto esito positivo o meno :) – Terry

2

Definirei sicuramente un errore 500 con un oggetto JSON che descrive la condizione di errore, simile a how an ASP.NET AJAX "ScriptService" error returns. Credo che questo sia abbastanza standard. È decisamente gradevole avere questa coerenza quando si gestiscono condizioni di errore potenzialmente inattese.

A parte questo, perché non utilizzare la funzionalità integrata in .NET, se la si scrive in C#? I servizi WCF e ASMX semplificano la serializzazione dei dati come JSON, senza reinventare la ruota.

+0

Non riesco pensa che in questo contesto dovrebbe essere usato il codice di errore 500. In base alle specifiche: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html, l'alternativa migliore è inviare 400 (richiesta errata). L'errore 500 è più adatto a un'eccezione non gestita. –

1

Se l'utente fornisce dati non validi, si dovrebbe assolutamente essere un 400 Bad Request (La richiesta contiene sintassi di cattivo o non può essere soddisfatta.)

+0

QUALSIASI della gamma 400 è accettabile e 422 è l'opzione migliore per i dati che non possono essere elaborati – jamesc

2

Rails ponteggi usano 422 Unprocessable Entity per questi tipi di errori. Vedere RFC 4918 per ulteriori informazioni.

3

Ho passato alcune ore a risolvere questo problema. La mia soluzione si basa sui seguenti desideri/requisiti:

  • Non avere codice di gestione errori ripetitivo in tutte le azioni del controller JSON.
  • Conserva i codici di stato HTTP (errore). Perché? Perché le preoccupazioni di livello più elevato non dovrebbero influire sull'implementazione di livello inferiore.
  • Essere in grado di ottenere dati JSON quando si verifica un errore/un'eccezione sul server. Perché? Perché potrei volere informazioni dettagliate sull'errore. Per esempio. messaggio di errore, codice di stato dell'errore specifico del dominio, traccia dello stack (nell'ambiente di debug/sviluppo).
  • Facilità di utilizzo lato client - preferibile utilizzando jQuery.

Creo un oggetto HandleErrorAttribute (vedere i commenti del codice per la spiegazione dei dettagli). Alcuni dettagli tra cui "usings" sono stati omessi, quindi il codice potrebbe non essere compilato. Aggiungo il filtro per i filtri globali durante l'inizializzazione dell'applicazione in Global.asax.cs come questo:

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

Attributo:

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

ho sviluppato il seguente plugin per jQuery per il lato client facilità d'uso :

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

Cosa ottengo da tutto questo? Il risultato finale è

  • Nessuna delle azioni del mio controller ha requisiti su HandleErrorAttributes.
  • Nessuna delle azioni del mio controller contiene codice di gestione degli errori ripetitivo della piastra della caldaia.
  • Ho un unico codice di gestione degli errori che consente di modificare facilmente la registrazione e altri elementi relativi alla gestione degli errori.
  • Un semplice requisito: le azioni del controller che restituiscono JsonResult devono avere il tipo restituito JsonResult e non un tipo di base come ActionResult. Motivo: vedi il commento del codice in FooHandleErrorAttribute. esempio lato

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

commenti sono i benvenuti! Io probabilmente blog su questa soluzione, un giorno ...

+0

boooo! è meglio avere una risposta semplice con solo una spiegazione necessaria di una risposta enorme per il gusto di soddisfare una situazione specifica. vai per una risposta generale la prossima volta, in modo che tutti possano usarlo – pythonian29033

0

Per gli errori del server/protocollo vorrei cercare di essere il più REST/HTTP possibile (Confrontare questo con voi a digitare l'URL del nel tuo browser):

  • un elemento non esistente viene chiamato (/ persone/{non-esistente-id-qui}). Restituire un 404.
  • si è verificato un errore imprevisto sul server (errore di codice). Restituire un 500.
  • l'utente client non è autorizzato a ottenere la risorsa. Restituire un 401.

Per errori specifici di dominio/business logic direi che il protocollo è utilizzato nel modo giusto e non c'è alcun errore interno del server, quindi rispondere con un errore oggetto JSON/XML o qualsiasi cosa si preferisca descrivere i vostri dati con (Confronta questo con voi la compilazione di moduli su un sito web):

  • un utente vuole cambiare il suo nome di account, ma l'utente non ha ancora verificare il suo account cliccando un link in una e-mail che è stato inviato a l'utente. Restituzione {"errore": "Account non verificato"} o altro.
  • un utente desidera ordinare un libro, ma il libro è stato venduto (stato modificato in DB) e non può più essere ordinato. Return {"error": "Book already sold"}.
0

Sì, è necessario utilizzare i codici di stato HTTP.E anche preferibilmente tornare descrizioni degli errori in un formato un po 'standardizzato JSON, come Nottingham’s proposal, vedi apigility Error Reporting:

The payload of an API Problem has the following structure:

  • type: a URL to a document describing the error condition (optional, and "about:blank" is assumed if none is provided; should resolve to a human-readable document; Apigility always provides this).
  • title: a brief title for the error condition (required; and should be the same for every problem of the same type; Apigility always provides this).
  • status: the HTTP status code for the current request (optional; Apigility always provides this).
  • detail: error details specific to this request (optional; Apigility requires it for each problem).
  • instance: URI identifying the specific instance of this problem (optional; Apigility currently does not provide this).