Meilleure pratique pour renvoyer les erreurs dans ASP.NET API Web

j'ai des inquiétudes sur la façon dont nous retournons les erreurs au client.

Ne nous renvoyer l'erreur immédiatement en lançant des HttpResponseException quand nous obtenons un message d'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 nous les 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);
}

ce n'est qu'un exemple de code, peu importe que ce soit une erreur de validation ou une erreur de serveur, Je voudrais juste connaître la meilleure pratique, les avantages et contre de chaque approche.

295
demandé sur Guido Leenders 2012-05-24 11:00:13

11 réponses

pour moi, je renvoie habituellement un HttpResponseException et je règle le code de statut en conséquence en fonction de l'exception lancée et si l'exception est fatale ou non déterminera si je renvoie le HttpResponseException immédiatement.

à la fin de la journée c'est une API qui renvoie des réponses et non des vues, donc je pense qu'il est bon de renvoyer un message avec l'exception et le code de statut au consommateur. Je n'ai pas eu besoin d'accumuler des erreurs et de les renvoyer comme la plupart les exceptions sont généralement dues à des paramètres ou des appels incorrects, etc.

un exemple dans mon application est que parfois le client demandera des données, mais il n'y a pas de données disponibles, donc je lance une noDataAvailableException personnalisée et la laisse bulle à l'application de l'api web, où alors dans mon filtre personnalisé qui la capture en renvoyant un message pertinent avec le code d'état correct.

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

mise à Jour :

depuis que j'ai répondu à cette question, quelques billets 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 constructions nocturnes) http://blogs.msdn.com/b/youssefm/archive/2012/06/28/error-handling-in-asp-net-webapi.aspx

Update 2

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

  1. pour des erreurs générales comme non trouvé, ou des paramètres invalides étant passés à une action, nous retournons une exception HttpResponseException pour arrêter le traitement immédiatement. En outre, pour les erreurs de modèle dans notre actions nous allons remettre le dictionnaire d'État modèle à l'extension Request.CreateErrorResponse et l'envelopper dans une exception de HttpResponseException. L'ajout du dictionnaire de l'état du modèle donne une liste des erreurs du 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 laissons la bulle d'exception à L'application API Web, ici nous avons un filtre d'exception globale qui regarde l'exception, les journaux avec elmah et trys pour faire sens de lui définition du code d'état http correct et d'un message d'erreur convivial pertinent en tant que corps à nouveau dans une exception HttpResponseException. Pour les exceptions que nous ne nous attendons pas à ce que le client reçoive l'erreur par défaut du serveur interne 500, mais un message générique pour des raisons de sécurité.

Update 3

récemment, après avoir récupéré L'API Web 2, Pour renvoyer les erreurs générales, nous utilisons maintenant le IHttpActionResult interface, spécifiquement les classes intégrées pour dans le système.Web.Http.Espace de noms de résultats tels que NotFound, BadRequest quand ils s'adaptent, s'ils ne nous les étendons pas, par exemple un résultat notfound 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);
    }
}
239
répondu gdp 2014-07-10 16:44:47

ASP.NET Web API 2 vraiment simplifié. 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'article 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 jetez pas L'erreur HTTP 500 à moins qu'il n'y ait une erreur catastrophique (par exemple, WCF Fault Exception). Choisissez un code de statut HTTP approprié qui représente l'état de vos données. (Voir le lien apigee ci-dessous.)

liens:

150
répondu Manish Jain 2014-09-22 21:56:01

il semble que vous ayez plus de problèmes avec la Validation que les erreurs/exceptions donc je vais dire un peu sur les deux.

Validation

Les actions du contrôleur

doivent généralement prendre des modèles D'entrée où la validation est déclarée directement sur le modèle.

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

vous pouvez alors utiliser un ActionFilter qui renvoie automatiquement les 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'information sur ce check-out http://ben.onfabrik.com/posts/automatic-modelstate-validation-in-aspnet-mvc

traitement des erreurs

il est préférable de retourner un message au client qui représente l'exception qui s'est produite (avec le code de statut pertinent).

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

je crée habituellement mon propre type d'exception" Sécuritaire " que je m'attends à ce que le client sache comment traiter et envelopper tous les autres avec une erreur générique de 500.

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

alors vous pouvez l'enregistrer globalement.

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

C'est ma coutume type d'exception.

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)
    {
    }
}
61
répondu Daniel Little 2018-03-23 04:06:27

vous pouvez lancer un HttpResponseException

HttpResponseMessage response = 
    this.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "your message");
throw new HttpResponseException(response);
29
répondu tartakynov 2014-03-04 05:15:18

pour L'API Web 2 mes méthodes renvoient systématiquement IHttpActionResult donc j'utilise...

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

    return ResponseMessage(
        Request.CreateResponse(
            HttpStatusCode.BadRequest, 
            validationErrors));
}
14
répondu Mick 2017-12-03 23:49:45

vous pouvez utiliser ActionFilter personnalisé dans L'Api Web 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();

}

Enregistrer CustomAttribute classe dans webApiConfig.cs config.Filtrer.Add(new DRFValidationFilters ());

4
répondu LokeshChikkala 2016-03-18 09:50:23

construire sur Manish Jain 'S réponse (qui est destiné à L'API Web 2 qui simplifie les choses):

1) utiliser structures de validation pour répondre au plus grand nombre possible d'erreurs de validation. Ces structures peuvent également être utilisées pour répondre aux demandes provenant des 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 ValidationResult s, que l'opération soit réussie 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) API Contrôleur construire la réponse basée sur la fonction de service résultat

une option est de mettre pratiquement tous les paramètres en option et d'effectuer une validation personnalisée qui renvoie une réponse plus significative. De plus, je prends soin de ne pas permettre qu'une exception aille au-delà des 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"));
        }
    }
2
répondu Alexei 2016-12-19 14:32:01

utilisez la méthode "Internalserverror" intégrée (disponible dans ApiController):

return InternalServerError();
//or...
return InternalServerError(new YourException("your message"));
1
répondu Rusty 2017-08-04 15:03:14

si vous utilisez ASP.NET API Web 2, la manière la plus simple est d'utiliser la méthode ApiController Short-Method. Il en résultera une BadRequestResult.

return BadRequest("message");
1
répondu Fabian von Ellerts 2018-03-22 12:22:48

juste pour mettre à jour sur l'état actuel de ASP.NET WebAPI. L'interface est maintenant appelée IActionResult et l'implémentation 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
répondu Thomas Hagström 2016-05-10 07:51:29

pour les erreurs où le modèle indique.isvalid est faux, en général, je envoyer l'erreur renvoyée par le code. C'est facile à comprendre pour le développeur qui consomme mon service. J'envoie généralement le résultat en utilisant le code ci-dessous.

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

cela envoie l'erreur au client dans le format ci-dessous qui est essentiellement une liste d'erreurs:

    [  
    "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)"
    ]
-2
répondu Ashish Sahu 2016-09-19 23:53:04