Meilleure pratique pour renvoyer des erreurs dans l'API Web ASP.NET

385

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

Renvoyons -nous l'erreur immédiatement 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 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 les erreurs de validation ou les erreurs de serveur, j'aimerais simplement connaître les meilleures pratiques, les avantages et les inconvénients de chaque approche.

cuongle
la source
1
Voir stackoverflow.com/a/22163675/200442 que vous devez utiliser ModelState.
Daniel Little
1
Notez que les réponses ici ne couvrent que les exceptions qui sont lancées dans le contrôleur lui-même. Si votre API retourne un <Modèle> IQueryable qui n'a pas encore été exécuté, l'exception n'est pas dans le contrôleur et n'est pas interceptée ...
Jess
3
Très belle question, mais d'une manière ou d'une autre je ne reçois aucune surcharge de constructeur de HttpResponseExceptionclasse qui prend deux paramètres mentionnés dans votre message - HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest) c'estHttpResponseException(string, HttpStatusCode)
RBT

Réponses:

293

Pour moi, je renvoie généralement un HttpResponseExceptionet définit le code d'état en fonction de l'exception levée et si l'exception est fatale ou non, je déterminerai si je renvoie HttpResponseExceptionimmédiatement.

À la fin de la journée, c'est une API qui renvoie des réponses et non des vues, donc je pense que c'est bien de renvoyer un message avec le code d'exception et de statut au consommateur. Je n'ai actuellement pas besoin d'accumuler des erreurs et 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 demandera des données, mais qu'il n'y a pas de données disponibles, je lance une personnalisation NoDataAvailableExceptionet la laisse bouillonner vers l'application Web API, où ensuite dans mon filtre personnalisé qui capture le renvoi d'un message pertinent avec le code d'état correct.

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

Mise à jour :

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

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

(celui-ci a de nouvelles fonctionnalités dans les versions nocturnes) https://docs.microsoft.com/archive/blogs/youssefm/error-handling-in-asp-net-webapi

Update 2

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

  1. Pour les erreurs générales telles que non trouvées ou les paramètres non valides transmis à une action, nous renvoyons un HttpResponseExceptionpour arrêter immédiatement le traitement. De plus, pour les erreurs de modèle dans nos actions, nous remettrons le dictionnaire d'état du modèle à l' Request.CreateErrorResponseextension et l'envelopperons dans a HttpResponseException. L'ajout du dictionnaire d'état du modèle entraîne une liste des erreurs de modèle envoyées dans le corps de 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 Web API, nous avons ici un filtre d'exception global qui examine l'exception, la consigne avec ELMAH et essaie de donner un sens à la définition du HTTP correct code d'état et un message d'erreur convivial pertinent que le corps à nouveau dans un HttpResponseException. Pour les 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 récupéré l'API Web 2, pour renvoyer des erreurs générales, nous utilisons maintenant l' interface IHttpActionResult , en particulier les classes intégrées dans l' System.Web.Http.Resultsespace de noms telles que NotFound, BadRequest lorsqu'elles conviennent, si elles ne le font pas, nous les étendons, 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);
    }
}
gdp
la source
Merci pour votre réponse geepie, c'est une bonne expérience, vous préférez donc envoyer expcetion immédiatement?
cuongle
Comme je l'ai dit, cela dépend vraiment de l'exception. Une exception fatale telle que, par exemple, l'utilisateur transmet à Web Api un paramètre non valide à un point de terminaison, puis je créerais une HttpResponseException et le retournerais immédiatement à l'application consommatrice.
gdp
Les exceptions dans la question concernent vraiment la validation, voir stackoverflow.com/a/22163675/200442 .
Daniel Little
1
@DanielLittle Relisez sa question. Je cite: "Ce n'est qu'un exemple de code, peu importe les erreurs de validation ou les erreurs de serveur"
gdp
@gdp Même s'il y a vraiment deux composants, la validation et les exceptions, il est donc préférable de couvrir les deux.
Daniel Little
185

L'API Web ASP.NET 2 l'a 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'élément est introuvable:

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 catastrophique (par exemple, exception de panne WCF). Choisissez un code d'état HTTP approprié qui représente l'état de vos données. (Voir le lien apigee ci-dessous.)

Liens:

Manish Jain
la source
4
Je voudrais aller plus loin et lancer une exception ResourceNotFoundException à partir du DAL / Repo que je vérifie dans Web Api 2.2 ExceptionHandler pour le type ResourceNotFoundException, puis je renvoie «Product with id xxx not found». De cette façon, son ancrage générique dans l'architecture au lieu de chaque action.
Pascal
1
Y a-t-il une utilisation spécifique pour la return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); Quelle est la différence entre CreateResponseetCreateErrorResponse
Zapnologica
11
Selon w3.org/Protocols/rfc2616/rfc2616-sec10.html , une erreur client est un code de niveau 400 et une erreur serveur est un code de niveau 500. Ainsi, un code d'erreur 500 peut être très approprié dans de nombreux cas pour une API Web, et pas seulement des erreurs "catastrophiques".
Jess
2
Vous avez besoin using System.Net.Http;que la CreateResponse()méthode d'extension apparaisse.
Adam Szabo
Ce que je n'aime pas à propos de Request.CreateResponse (), c'est qu'il renvoie des informations de sérialisation spécifiques à Microsoft inutiles comme "<string xmlns =" schemas.microsoft.com/2003/10/Serialization/">Mon erreur ici </ string > ". Pour les situations où l'état 400 est approprié, j'ai trouvé que ApiController.BadRequest (message de chaîne) renvoie une meilleure chaîne "<Error> <Message> Mon erreur ici </Message> </Error>". Mais je ne trouve pas son équivalent pour retourner le statut 500 avec un simple message.
vkelman
76

Il semble que vous ayez plus de problèmes avec la validation que d'erreurs / exceptions, je vais donc en 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 ensuite utiliser un ActionFilterqui renvoie 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 à ce sujet, consultez http://ben.onfabrik.com/posts/automatic-modelstate-validation-in-aspnet-mvc

La gestion des erreurs

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

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

Je crée généralement mon propre type d'exception «sûre» que je suppose que le client saura comment gérer et envelopper tous les autres avec une erreur générique 500.

L'utilisation d'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());

Il s'agit de 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 lever.

public class NotAuthenticatedException : ApiException
{
    public NotAuthenticatedException()
        : base(HttpStatusCode.Forbidden)
    {
    }
}
Daniel Little
la source
J'ai un problème lié à la réponse de gestion des erreurs dans la définition de classe ApiExceptionFilterAttribute. Dans la méthode OnException, l'exception.StatusCode n'est pas valide car l'exception est une WebException. Que puis-je faire dans ce cas?
razp26
1
@ razp26 si vous faites référence à ce var exception = context.Exception as WebException;qui était une faute de frappe, cela aurait dû l'êtreApiException
Daniel Little
2
Pouvez-vous ajouter un exemple d'utilisation de la classe ApiExceptionFilterAttribute?
razp26
36

Vous pouvez lever une HttpResponseException

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

Pour l'API Web 2, mes méthodes retournent systématiquement IHttpActionResult, donc j'utilise ...

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

    return ResponseMessage(
        Request.CreateResponse(
            HttpStatusCode.BadRequest, 
            validationErrors));
}
Mick
la source
Cette réponse est correcte, alors que vous devriez ajouter une référence àSystem.Net.Http
Bellash
20

Si vous utilisez l'API Web ASP.NET 2, la méthode la plus simple consiste à utiliser la méthode courte ApiController. Cela se traduira par un BadRequestResult.

return BadRequest("message");
Fabian von Ellerts
la source
Strictement pour les erreurs de validation de modèle, j'utilise la surcharge de BadRequest () qui accepte l'objet ModelState:return BadRequest(ModelState);
timmi4sa
4

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

LokeshChikkala
la source
4

S'appuyant sur Manish Jainla réponse de (qui est destinée à Web API 2 qui simplifie les choses):

1) Utilisez des structures de validation pour répondre à autant d'erreurs de validation que possible. Ces structures peuvent également être utilisées pour répondre à des 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) La couche de service renverra ValidationResults, que l'opération réussisse 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) Le contrôleur API construira la réponse en fonction du résultat de la fonction de service

Une option consiste à mettre pratiquement tous les paramètres en option et à effectuer une validation personnalisée qui renvoie une réponse plus significative. De plus, je prends soin de ne laisser aucune exception dépasser la limite de 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"));
        }
    }
Alexei
la source
3

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

return InternalServerError();
//or...
return InternalServerError(new YourException("your message"));
Rouillé
la source
0

Juste pour mettre à jour l'état actuel d'ASP.NET WebAPI. L'interface est maintenant appelée IActionResultet 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
}
Thomas Hagström
la source
Cela semble intéressant, mais où précisément dans le projet ce code est-il placé? Je fais mon projet web api 2 sur vb.net.
Off The Gold
C'est juste un modèle pour renvoyer l'erreur et peut résider n'importe où. Vous renverriez une nouvelle instance de la classe ci-dessus dans votre Controller. Mais pour être honnête, j'essaie d'utiliser les classes intégrées autant que possible: this.Ok (), CreatedAtRoute (), NotFound (). La signature de la méthode serait IHttpActionResult. Je ne sais pas s'ils ont changé tout cela avec NetCore
Thomas Hagström
-2

Pour les erreurs où modelstate.isvalid est faux, j'envoie généralement l'erreur telle qu'elle est 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)"
    ]
Ashish Sahu
la source
Je ne recommanderais pas de renvoyer ce niveau de détail dans l'exception s'il s'agissait d'une API externe (c'est-à-dire exposée à l'Internet public). Vous devriez faire un peu plus de travail dans le filtre et renvoyer un objet JSON (ou XML si c'est le format choisi) détaillant l'erreur plutôt qu'un simple ToString d'exception.
Sudhanshu Mishra
Corrigez de ne pas envoyer cette exception pour l'API externe. Mais vous pouvez l'utiliser pour déboguer des problèmes dans l'API interne et pendant les tests.
Ashish Sahu