Procedura consigliata per restituire errori nell'API Web ASP.NET


384

Ho delle preoccupazioni sul modo in cui restituiamo errori al cliente.

Restituiamo immediatamente l'errore lanciando HttpResponseException quando otteniamo un errore:

public void Post(Customer customer)
{
    if (string.IsNullOrEmpty(customer.Name))
    {
        throw new HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest) 
    }
    if (customer.Accounts.Count == 0)
    {
         throw new HttpResponseException("Customer does not have any account", HttpStatusCode.BadRequest) 
    }
}

Oppure accumuliamo tutti gli errori e li rimandiamo al client:

public void Post(Customer customer)
{
    List<string> errors = new List<string>();
    if (string.IsNullOrEmpty(customer.Name))
    {
        errors.Add("Customer Name cannot be empty"); 
    }
    if (customer.Accounts.Count == 0)
    {
         errors.Add("Customer does not have any account"); 
    }
    var responseMessage = new HttpResponseMessage<List<string>>(errors, HttpStatusCode.BadRequest);
    throw new HttpResponseException(responseMessage);
}

Questo è solo un codice di esempio, non importa né errori di convalida né errori del server, vorrei solo conoscere le migliori pratiche, i pro ei contro di ogni approccio.


1
Vedi stackoverflow.com/a/22163675/200442 che dovresti utilizzare ModelState.
Daniel Little

1
Si noti che le risposte qui riguardano solo le eccezioni generate nel controller stesso. Se la tua API restituisce un <modello> IQueryable che non è stato ancora eseguito, l'eccezione non è presente nel controller e non viene rilevata ...
Jess

3
Molto bella domanda ma in qualche modo non sto ottenendo alcun sovraccarico di costruttori di HttpResponseExceptionclasse che accetta due parametri menzionati nel tuo post - HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest) cioèHttpResponseException(string, HttpStatusCode)
RBT

Risposte:


293

Per me di solito rispedisco un HttpResponseExceptione imposto il codice di stato di conseguenza a seconda dell'eccezione generata e se l'eccezione è fatale o no determinerò se rispedisco HttpResponseExceptionimmediatamente.

Alla fine della giornata è un'API che invia risposte e non visualizzazioni, quindi penso che sia giusto rispedire un messaggio con l'eccezione e il codice di stato al consumatore. Al momento non ho bisogno di accumulare errori e rispedirli poiché la maggior parte delle eccezioni sono generalmente dovute a parametri o chiamate errati, ecc.

Un esempio nella mia app è che a volte il client chiederà dati, ma non ci sono dati disponibili, quindi lancio un'abitudine NoDataAvailableExceptione li lascio passare all'app Web API, dove poi nel mio filtro personalizzato che lo cattura restituendo un messaggio pertinente insieme al codice di stato corretto.

Non sono sicuro al 100% su quale sia la migliore pratica per questo, ma al momento sta funzionando per me, quindi è quello che sto facendo.

Aggiornamento :

Da quando ho risposto a questa domanda sono stati scritti alcuni post sul blog sull'argomento:

https://weblogs.asp.net/fredriknormen/asp-net-web-api-exception-handling

(questo ha alcune nuove funzionalità nelle build notturne) https://docs.microsoft.com/archive/blogs/youssefm/error-handling-in-asp-net-webapi

Aggiornamento 2

Aggiornamento al nostro processo di gestione degli errori, abbiamo due casi:

  1. Per errori generali come non trovati o parametri non validi passati a un'azione restituiamo a HttpResponseExceptionper interrompere immediatamente l'elaborazione. Inoltre, per errori del modello nelle nostre azioni, consegneremo il dizionario dello stato del modello Request.CreateErrorResponseall'estensione e lo racchiuderemo in a HttpResponseException. L'aggiunta del dizionario di stato del modello comporta un elenco degli errori del modello inviati nel corpo della risposta.

  2. Per gli errori che si verificano in livelli superiori, gli errori del server, lasciamo che l'eccezione avvenga sull'app dell'API Web, qui abbiamo un filtro di eccezioni globale che esamina l'eccezione, lo registra con ELMAH e cerca di dargli un senso impostando l'HTTP corretto codice di stato e un messaggio di errore descrittivo pertinente come nuovamente il corpo in a HttpResponseException. Ad eccezione del fatto che non prevediamo che il client riceverà l'errore 500 del server interno predefinito, ma un messaggio generico per motivi di sicurezza.

Aggiornamento 3

Di recente, dopo aver raccolto l'API Web 2, per restituire errori generali, ora utilizziamo l' interfaccia IHttpActionResult , in particolare le classi integrate nello System.Web.Http.Resultsspazio dei nomi come NotFound, BadRequest quando si adattano, se non li estendiamo, ad esempio un risultato NotFound con un messaggio di risposta:

public class NotFoundWithMessageResult : IHttpActionResult
{
    private string message;

    public NotFoundWithMessageResult(string message)
    {
        this.message = message;
    }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        var response = new HttpResponseMessage(HttpStatusCode.NotFound);
        response.Content = new StringContent(message);
        return Task.FromResult(response);
    }
}

Grazie per la tua risposta geepie, è una buona esperienza, quindi preferisci inviare immediatamente l'expcetion?
cuongle

Come ho già detto, dipende davvero dall'eccezione. Un'eccezione fatale come ad esempio l'utente passa alla Web Api un parametro non valido a un endpoint, quindi vorrei creare un HttpResponseException e restituirlo immediatamente all'app che consuma.
gdp

Le eccezioni alla domanda riguardano molto di più la convalida, vedere stackoverflow.com/a/22163675/200442 .
Daniel Little

1
@DanielLittle Rileggi la sua domanda. Cito: "Questo è solo un codice di esempio, non importa né errori di convalida né errori del server"
gdp

@gdp Anche così ci sono davvero due componenti: validazione ed eccezioni, quindi è meglio coprire entrambi.
Daniel Little

184

L'API Web ASP.NET 2 lo ha davvero semplificato. Ad esempio, il seguente codice:

public HttpResponseMessage GetProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        var message = string.Format("Product with id = {0} not found", id);
        HttpError err = new HttpError(message);
        return Request.CreateResponse(HttpStatusCode.NotFound, err);
    }
    else
    {
        return Request.CreateResponse(HttpStatusCode.OK, item);
    }
}

restituisce il seguente contenuto al browser quando l'elemento non viene trovato:

HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
Date: Thu, 09 Aug 2012 23:27:18 GMT
Content-Length: 51

{
  "Message": "Product with id = 12 not found"
}

Suggerimento: non generare l'errore HTTP 500 a meno che non si verifichi un errore catastrofico (ad esempio Eccezione errore WCF). Scegli un codice di stato HTTP appropriato che rappresenti lo stato dei tuoi dati. (Vedi il link apigee di seguito.)

link:


4
Vorrei fare un ulteriore passo avanti e lanciare una ResourceNotFoundException dal DAL / Repo che controllo in Api 2.2 ExceptionHandler Web per Type ResourceNotFoundException e quindi restituisco "Prodotto con ID xxx non trovato". In questo modo è generalmente ancorato nell'architettura invece di ogni azione.
Pascal,

1
C'è un uso specifico per return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); Qual è la differenza tra CreateResponseeCreateErrorResponse
Zapnologica

10
Secondo, w3.org/Protocols/rfc2616/rfc2616-sec10.html , un errore client è un codice di livello 400 e un errore del server è un codice di livello 500. Quindi un codice di errore 500 potrebbe essere molto appropriato in molti casi per un'API Web, non solo errori "catastrofici".
Jess,

2
Devi visualizzare using System.Net.Http;il CreateResponse()metodo di estensione.
Adam Szabo,

Quello che non mi piace dell'utilizzo di Request.CreateResponse () è che restituisce informazioni di serializzazione specifiche di Microsoft non necessarie come "<string xmlns =" schemas.microsoft.com/2003/10/Serialization/">Il mio errore qui </ string >". Per le situazioni in cui lo stato 400 è appropriato, ho scoperto che ApiController.BadRequest (messaggio stringa) restituisce una stringa "<Error> <Message> il mio errore qui </Message> </Error>" migliore. Ma non riesco a trovare il suo equivalente per restituire lo stato 500 con un semplice messaggio.
vkelman,

76

Sembra che tu abbia più problemi con la convalida che errori / eccezioni, quindi parlerò un po 'di entrambi.

Validazione

Le azioni del controller dovrebbero generalmente prendere modelli di input in cui la convalida è dichiarata direttamente sul modello.

public class Customer
{ 
    [Require]
    public string Name { get; set; }
}

Quindi è possibile utilizzare un ActionFilterche invia automaticamente i messaggi di convalida al client.

public class ValidationActionFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var modelState = actionContext.ModelState;

        if (!modelState.IsValid) {
            actionContext.Response = actionContext.Request
                 .CreateErrorResponse(HttpStatusCode.BadRequest, modelState);
        }
    }
} 

Per ulteriori informazioni su questo, consultare http://ben.onfabrik.com/posts/automatic-modelstate-validation-in-aspnet-mvc

Gestione degli errori

È meglio restituire un messaggio al client che rappresenta l'eccezione che si è verificata (con il relativo codice di stato).

Immediatamente devi usare Request.CreateErrorResponse(HttpStatusCode, message)se vuoi specificare un messaggio. Tuttavia, questo lega il codice Requestall'oggetto, cosa che non dovresti fare.

Di solito creo il mio tipo di eccezione "sicura" che mi aspetto che il client sappia come gestire e avvolgere tutti gli altri con un errore 500 generico.

L'utilizzo di un filtro azioni per gestire le eccezioni sarebbe simile al seguente:

public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{
    public override void OnException(HttpActionExecutedContext context)
    {
        var exception = context.Exception as ApiException;
        if (exception != null) {
            context.Response = context.Request.CreateErrorResponse(exception.StatusCode, exception.Message);
        }
    }
}

Quindi puoi registrarlo a livello globale.

GlobalConfiguration.Configuration.Filters.Add(new ApiExceptionFilterAttribute());

Questo è il mio tipo di eccezione personalizzato.

using System;
using System.Net;

namespace WebApi
{
    public class ApiException : Exception
    {
        private readonly HttpStatusCode statusCode;

        public ApiException (HttpStatusCode statusCode, string message, Exception ex)
            : base(message, ex)
        {
            this.statusCode = statusCode;
        }

        public ApiException (HttpStatusCode statusCode, string message)
            : base(message)
        {
            this.statusCode = statusCode;
        }

        public ApiException (HttpStatusCode statusCode)
        {
            this.statusCode = statusCode;
        }

        public HttpStatusCode StatusCode
        {
            get { return this.statusCode; }
        }
    }
}

Un'eccezione di esempio che la mia API può generare.

public class NotAuthenticatedException : ApiException
{
    public NotAuthenticatedException()
        : base(HttpStatusCode.Forbidden)
    {
    }
}

Ho un problema relativo alla risposta di gestione degli errori nella definizione della classe ApiExceptionFilterAttribute. Nel metodo OnException, exception.StatusCode non è valido poiché l'eccezione è WebException. Cosa posso fare in questo caso?
razp26,

1
@ razp26 se ti riferisci al tipo var exception = context.Exception as WebException;che era un refuso, avrebbe dovuto essereApiException
Daniel Little,

2
Potete per favore aggiungere un esempio di come verrebbe utilizzata la classe ApiExceptionFilterAttribute?
razp26,

36

Puoi lanciare una HttpResponseException

HttpResponseMessage response = 
    this.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "your message");
throw new HttpResponseException(response);

23

Per l'API Web 2 i miei metodi restituiscono costantemente IHttpActionResult, quindi utilizzo ...

public IHttpActionResult Save(MyEntity entity)
{
  ....

    return ResponseMessage(
        Request.CreateResponse(
            HttpStatusCode.BadRequest, 
            validationErrors));
}

Questa risposta va bene, mentre dovresti aggiungere un riferimento aSystem.Net.Http
Bellash,

20

Se si utilizza ASP.NET Web API 2, il modo più semplice è utilizzare il metodo breve ApiController. Ciò comporterà un BadRequestResult.

return BadRequest("message");

A rigor di errori di convalida del modello, utilizzo il sovraccarico di BadRequest () che accetta l'oggetto ModelState:return BadRequest(ModelState);
timmi4sa,

4

è possibile utilizzare ActionFilter personalizzato in API Web per convalidare il modello

public class DRFValidationFilters : ActionFilterAttribute
{

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!actionContext.ModelState.IsValid)
        {
            actionContext.Response = actionContext.Request
                 .CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);

            //BadRequest(actionContext.ModelState);
        }
    }
    public override Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
    {

        return Task.Factory.StartNew(() => {

            if (!actionContext.ModelState.IsValid)
            {
                actionContext.Response = actionContext.Request
                     .CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);                    
            }
        });

    }

public class AspirantModel
{
    public int AspirantId { get; set; }
    public string FirstName { get; set; }
    public string MiddleName { get; set; }        
    public string LastName { get; set; }
    public string AspirantType { get; set; }       
    [RegularExpression(@"^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$", ErrorMessage = "Not a valid Phone number")]
    public string MobileNumber { get; set; }
    public int StateId { get; set; }
    public int CityId { get; set; }
    public int CenterId { get; set; }

}

    [HttpPost]
    [Route("AspirantCreate")]
    [DRFValidationFilters]
    public IHttpActionResult Create(AspirantModel aspirant)
    {
            if (aspirant != null)
            {

            }
            else
            {
                return Conflict();
            }
          return Ok();

}

Registrare la classe CustomAttribute in webApiConfig.cs config.Filters.Add (new DRFValidationFilters ());


4

Sulla base Manish Jaindella risposta (che è pensata per l'API Web 2 che semplifica le cose):

1) Utilizzare le strutture di validazione per rispondere al maggior numero possibile di errori di validazione. Queste strutture possono anche essere utilizzate per rispondere a richieste provenienti da moduli.

public class FieldError
{
    public String FieldName { get; set; }
    public String FieldMessage { get; set; }
}

// a result will be able to inform API client about some general error/information and details information (related to invalid parameter values etc.)
public class ValidationResult<T>
{
    public bool IsError { get; set; }

    /// <summary>
    /// validation message. It is used as a success message if IsError is false, otherwise it is an error message
    /// </summary>
    public string Message { get; set; } = string.Empty;

    public List<FieldError> FieldErrors { get; set; } = new List<FieldError>();

    public T Payload { get; set; }

    public void AddFieldError(string fieldName, string fieldMessage)
    {
        if (string.IsNullOrWhiteSpace(fieldName))
            throw new ArgumentException("Empty field name");

        if (string.IsNullOrWhiteSpace(fieldMessage))
            throw new ArgumentException("Empty field message");

        // appending error to existing one, if field already contains a message
        var existingFieldError = FieldErrors.FirstOrDefault(e => e.FieldName.Equals(fieldName));
        if (existingFieldError == null)
            FieldErrors.Add(new FieldError {FieldName = fieldName, FieldMessage = fieldMessage});
        else
            existingFieldError.FieldMessage = $"{existingFieldError.FieldMessage}. {fieldMessage}";

        IsError = true;
    }

    public void AddEmptyFieldError(string fieldName, string contextInfo = null)
    {
        AddFieldError(fieldName, $"No value provided for field. Context info: {contextInfo}");
    }
}

public class ValidationResult : ValidationResult<object>
{

}

2) Il livello di servizio restituirà ValidationResults, indipendentemente dal fatto che l'operazione abbia esito positivo o meno. Per esempio:

    public ValidationResult DoSomeAction(RequestFilters filters)
    {
        var ret = new ValidationResult();

        if (filters.SomeProp1 == null) ret.AddEmptyFieldError(nameof(filters.SomeProp1));
        if (filters.SomeOtherProp2 == null) ret.AddFieldError(nameof(filters.SomeOtherProp2 ), $"Failed to parse {filters.SomeOtherProp2} into integer list");

        if (filters.MinProp == null) ret.AddEmptyFieldError(nameof(filters.MinProp));
        if (filters.MaxProp == null) ret.AddEmptyFieldError(nameof(filters.MaxProp));


        // validation affecting multiple input parameters
        if (filters.MinProp > filters.MaxProp)
        {
            ret.AddFieldError(nameof(filters.MinProp, "Min prop cannot be greater than max prop"));
            ret.AddFieldError(nameof(filters.MaxProp, "Check"));
        }

        // also specify a global error message, if we have at least one error
        if (ret.IsError)
        {
            ret.Message = "Failed to perform DoSomeAction";
            return ret;
        }

        ret.Message = "Successfully performed DoSomeAction";
        return ret;
    }

3) Il controller API costruirà la risposta in base al risultato della funzione di servizio

Un'opzione è quella di mettere praticamente tutti i parametri come opzionali ed eseguire una convalida personalizzata che restituisca una risposta più significativa. Inoltre, sto facendo attenzione a non consentire alcuna eccezione per andare oltre il limite del servizio.

    [Route("DoSomeAction")]
    [HttpPost]
    public HttpResponseMessage DoSomeAction(int? someProp1 = null, string someOtherProp2 = null, int? minProp = null, int? maxProp = null)
    {
        try
        {
            var filters = new RequestFilters 
            {
                SomeProp1 = someProp1 ,
                SomeOtherProp2 = someOtherProp2.TrySplitIntegerList() ,
                MinProp = minProp, 
                MaxProp = maxProp
            };

            var result = theService.DoSomeAction(filters);
            return !result.IsError ? Request.CreateResponse(HttpStatusCode.OK, result) : Request.CreateResponse(HttpStatusCode.BadRequest, result);
        }
        catch (Exception exc)
        {
            Logger.Log(LogLevel.Error, exc, "Failed to DoSomeAction");
            return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, new HttpError("Failed to DoSomeAction - internal error"));
        }
    }

3

Utilizzare il metodo "InternalServerError" incorporato (disponibile in ApiController):

return InternalServerError();
//or...
return InternalServerError(new YourException("your message"));

0

Solo per aggiornare sullo stato corrente di ASP.NET WebAPI. L'interfaccia è ora chiamata IActionResulte l'implementazione non è cambiata molto:

[JsonObject(IsReference = true)]
public class DuplicateEntityException : IActionResult
{        
    public DuplicateEntityException(object duplicateEntity, object entityId)
    {
        this.EntityType = duplicateEntity.GetType().Name;
        this.EntityId = entityId;
    }

    /// <summary>
    ///     Id of the duplicate (new) entity
    /// </summary>
    public object EntityId { get; set; }

    /// <summary>
    ///     Type of the duplicate (new) entity
    /// </summary>
    public string EntityType { get; set; }

    public Task ExecuteResultAsync(ActionContext context)
    {
        var message = new StringContent($"{this.EntityType ?? "Entity"} with id {this.EntityId ?? "(no id)"} already exist in the database");

        var response = new HttpResponseMessage(HttpStatusCode.Ambiguous) { Content = message };

        return Task.FromResult(response);
    }

    #endregion
}

Sembra interessante, ma in quale parte del progetto viene inserito questo codice? Sto facendo il mio progetto web api 2 in vb.net.
Off The Gold,

È solo un modello per restituire l'errore e può risiedere ovunque. Restituiresti una nuova istanza della classe sopra nel tuo controller. Ma ad essere sincero, cerco di usare le classi integrate ogni volta che è possibile: this.Ok (), CreatedAtRoute (), NotFound (). La firma del metodo sarebbe IHttpActionResult. Non so se hanno cambiato tutto questo con NetCore
Thomas Hagström il

-2

Per quegli errori in cui modelstate.isvalid è falso, generalmente invio l'errore quando viene generato dal codice. È facile da capire per lo sviluppatore che sta consumando il mio servizio. In genere invio il risultato utilizzando il codice seguente.

     if(!ModelState.IsValid) {
                List<string> errorlist=new List<string>();
                foreach (var value in ModelState.Values)
                {
                    foreach(var error in value.Errors)
                    errorlist.Add( error.Exception.ToString());
                    //errorlist.Add(value.Errors);
                }
                HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.BadRequest,errorlist);}

Questo invia l'errore al client nel seguente formato che è sostanzialmente un elenco di errori:

    [  
    "Newtonsoft.Json.JsonReaderException: **Could not convert string to integer: abc. Path 'Country',** line 6, position 16.\r\n   
at Newtonsoft.Json.JsonReader.ReadAsInt32Internal()\r\n   
at Newtonsoft.Json.JsonTextReader.ReadAsInt32()\r\n   
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadForType(JsonReader reader, JsonContract contract, Boolean hasConverter, Boolean inArray)\r\n   
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)",

       "Newtonsoft.Json.JsonReaderException: **Could not convert string to integer: ab. Path 'State'**, line 7, position 13.\r\n   
at Newtonsoft.Json.JsonReader.ReadAsInt32Internal()\r\n   
at Newtonsoft.Json.JsonTextReader.ReadAsInt32()\r\n   
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadForType(JsonReader reader, JsonContract contract, Boolean hasConverter, Boolean inArray)\r\n   
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)"
    ]

Non consiglierei di rispedire questo livello di dettaglio nell'eccezione se si trattasse di un'API esterna (es. Esposta a Internet pubblica). Dovresti fare un po 'di lavoro in più nel filtro e rispedire indietro un oggetto JSON (o XML se è il formato scelto) che descriva dettagliatamente l'errore piuttosto che solo una ToString di eccezione.
Sudhanshu Mishra,

Corretto non ha inviato questa eccezione per API esterna. Ma puoi usarlo per eseguire il debug dei problemi nell'API interna e durante i test.
Ashish Sahu,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.