web-dev-qa-db-fra.com

Méthode recommandée pour renvoyer les erreurs dans l'API Web ASP.NET

Je suis préoccupé par la façon dont nous renvoyons les erreurs au client.

Renvoyons-nous immédiatement l'erreur en lançant HttpResponseException lorsque nous obtenons une erreur:

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

Ou nous accumulons toutes les erreurs puis renvoyons au 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);
}

Il ne s'agit que d'un exemple de code; les erreurs de validation ou les erreurs de serveur n'ont pas d'importance. J'aimerais simplement connaître les meilleures pratiques, le pour et le contre de chaque approche.

347
cuongle

Pour moi, je renvoie habituellement un HttpResponseException et je règle le code d'état en fonction de l'exception levée. Si l'exception est fatale ou non, cela déterminera si je renvoie le HttpResponseException immédiatement.

À la fin de la journée, il s’agit d’une API renvoyant des réponses et non des vues. Je pense donc que c’est bien de renvoyer un message contenant l’exception et le code de statut au consommateur. Je n'ai actuellement pas besoin d'accumuler des erreurs ni de les renvoyer, car la plupart des exceptions sont généralement dues à des paramètres ou des appels incorrects, etc.

Un exemple dans mon application est que, parfois, le client demande des données, mais il n’ya pas de données disponibles. Je jette donc une exception noDataAvailableException personnalisée et la laisse apparaître sur l’application de l’API Web, puis dans mon filtre personnalisé qui le capture en renvoyant un message pertinent. message avec le code de statut correct.

Je ne suis pas sûr à 100% de la meilleure pratique à cet égard, mais cela fonctionne pour moi actuellement, alors c'est ce que je fais.

Mise à jour :

Depuis que j'ai répondu à cette question, quelques articles de blog ont été écrits sur le sujet:

http://weblogs.asp.net/fredriknormen/archive/2012/06/11/asp-net-web-api-exception-handling.aspx

(Celui-ci a quelques nouvelles fonctionnalités dans les versions nocturnes) http://blogs.msdn.com/b/youssefm/archive/2012/06/28/error-handling-in-asp-net-webapi. aspx

Mise à jour 2

Mise à jour de notre processus de traitement des erreurs, nous avons deux cas:

  1. Pour les erreurs générales telles que non trouvé ou les paramètres non valides transmis à une action, nous renvoyons une exception HttpResponseException pour arrêter le traitement immédiatement. De plus, pour les erreurs de modèle dans nos actions, nous allons transférer le dictionnaire d'état de modèle à l'extension Request.CreateErrorResponse et l'envelopper dans une exception HttpResponseException. L'ajout du dictionnaire d'état du modèle génère une liste des erreurs de modèle envoyées dans le corps de la réponse.

  2. Pour les erreurs qui se produisent dans les couches supérieures, les erreurs de serveur, nous avons laissé l'exception échapper dans l'application de l'API Web. Nous avons ici un filtre d'exception global qui examine les exceptions, les enregistre avec elmah et essaie de le comprendre en définissant le code http correct. code de statut et un message d'erreur convivial pertinent comme corps à nouveau dans une exception HttpResponseException. Pour des exceptions que nous n'attendons pas, le client recevra l'erreur de serveur interne 500 par défaut, mais un message générique pour des raisons de sécurité.

Mise à jour 3

Récemment, après avoir sélectionné Web API 2, pour renvoyer les erreurs générales, nous utilisons maintenant l'interface IHttpActionResult , en particulier les classes intégrées de l'espace de noms System.Web.Http.Results, tel que NotFound, BadRequest when ils correspondent, s'ils ne sont pas étendus, par exemple un résultat non trouvé avec un message de réponse:

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);
    }
}
274
gdp

L'API Web ASP.NET 2 l'a vraiment simplifiée. Par exemple, le code suivant:

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

renvoie le contenu suivant au navigateur lorsque l'élément n'est pas trouvé:

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

Suggestion: Ne lancez pas l'erreur HTTP 500 sauf en cas d'erreur grave (par exemple, exception de faute WCF). Choisissez un code d'état HTTP approprié représentant l'état de vos données. (Voir le lien apigee ci-dessous.)

Liens:

171
Manish Jain

Il semble que vous rencontriez plus de problèmes avec la validation qu'avec des erreurs/exceptions, je vais donc en dire un peu plus sur les deux.

Validation

Les actions du contrôleur doivent généralement prendre des modèles d'entrée dans lesquels la validation est déclarée directement sur le modèle.

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

Vous pouvez ensuite utiliser un ActionFilter qui envoie automatiquement des messages de validation au 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);
        }
    }
} 

Pour plus d'informations sur cette vérification http://ben.onfabrik.com/posts/automatic-modelstate-validation-in-aspnet-mvc

Traitement des erreurs

Il est préférable de renvoyer au client un message qui représente l'exception qui s'est produite (avec le code d'état correspondant).

Hors de la boîte, vous devez utiliser Request.CreateErrorResponse(HttpStatusCode, message) si vous souhaitez spécifier un message. Cependant, cela lie le code à l'objet Request, ce que vous ne devriez pas avoir à faire.

Je crée habituellement mon propre type d’exception "sûre", pour que le client sache comment gérer et envelopper toutes les autres avec une erreur générique 500.

Utiliser un filtre d'action pour gérer les exceptions ressemblerait à ceci:

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

Ensuite, vous pouvez l'enregistrer globalement.

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

Ceci est mon type d'exception personnalisé.

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 exemple d'exception que mon API peut lancer.

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

Vous pouvez lancer une exception HttpResponseException

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

Pour la Web API 2, mes méthodes renvoient systématiquement IHttpActionResult, donc je l'utilise ...

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

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

Si vous utilisez ASP.NET Web API 2, le moyen le plus simple consiste à utiliser la méthode abrégée ApiController. Cela entraînera un BadRequestResult.

return BadRequest("message");
15
Fabian von Ellerts

vous pouvez utiliser ActionFilter personnalisé dans Web Api pour valider le modèle

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

}

Enregistrez la classe CustomAttribute dans webApiConfig.cs config.Filters.Add (new DRFValidationFilters ());

4
LokeshChikkala

Construire sur la réponse de Manish Jain (qui est destiné à Web API 2, ce qui simplifie les choses):

1) Utilisez structures de validation pour répondre à autant d’erreurs de validation que possible. Ces structures peuvent également être utilisées pour répondre aux demandes provenant de formulaires.

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) couche de service retournera ValidationResults, que l'opération ait abouti ou non. Par exemple:

    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) Contrôleur d'API construira la réponse en fonction du résultat de la fonction de service

Une option consiste à définir virtuellement tous les paramètres et à effectuer une validation personnalisée qui renvoie une réponse plus significative. De plus, je veille à ce qu'aucune exception ne dépasse les limites du service.

    [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"));
        }
    }
4
Alexei

Utilisez la méthode "InternalServerError" intégrée (disponible dans ApiController):

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

Juste pour mettre à jour sur l'état actuel de ASP.NET WebAPI. L'interface s'appelle maintenant IActionResult et la mise en oeuvre n'a pas beaucoup changé:

[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
}
0
Thomas Hagström