Práctica recomendada para devolver errores en la API web ASP.NET

385

Me preocupa la forma en que devolvemos los errores al cliente.

¿Devuelve el error de inmediato lanzando HttpResponseException cuando recibimos un error:

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

O acumulamos todos los errores y luego los enviamos de vuelta al cliente:

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

Este es solo un código de muestra, no importa ni los errores de validación ni los errores del servidor, solo me gustaría conocer las mejores prácticas, los pros y los contras de cada enfoque.

cuongle
fuente
1
Consulte stackoverflow.com/a/22163675/200442 que debería usar ModelState.
Daniel Little
1
Tenga en cuenta que las respuestas aquí solo cubren las excepciones que se generan en el controlador. Si su API devuelve un IQueryable <Model> que aún no se ha ejecutado, la excepción no está en el controlador y no se detecta ...
Jess
3
Muy buena pregunta, pero de alguna manera no obtengo ninguna sobrecarga de constructor de HttpResponseExceptionclase que tome dos parámetros mencionados en su publicación, HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest) es decirHttpResponseException(string, HttpStatusCode)
RBT

Respuestas:

293

Para mí, generalmente envío de vuelta HttpResponseExceptiony configuro el código de estado en consecuencia, dependiendo de la excepción lanzada y si la excepción es fatal o no, determinaré si devuelvo el HttpResponseExceptioninmediatamente.

Al final del día, es una API que envía respuestas y no vistas, así que creo que está bien enviar un mensaje con el código de excepción y estado al consumidor. Actualmente no he necesitado acumular errores y enviarlos de regreso, ya que la mayoría de las excepciones generalmente se deben a parámetros o llamadas incorrectas, etc.

Un ejemplo en mi aplicación es que a veces el cliente pedirá datos, pero no hay datos disponibles, por lo que lanzo un mensaje personalizado NoDataAvailableExceptiony lo dejo en la aplicación API web, donde luego en mi filtro personalizado que captura el envío de un mensaje relevante junto con el código de estado correcto.

No estoy 100% seguro de cuál es la mejor práctica para esto, pero esto está funcionando para mí actualmente, así que eso es lo que estoy haciendo.

Actualización :

Desde que respondí esta pregunta, se han escrito algunas publicaciones de blog sobre el tema:

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

(este tiene algunas características nuevas en las versiones nocturnas) https://docs.microsoft.com/archive/blogs/youssefm/error-handling-in-asp-net-webapi

Actualización 2

Actualización de nuestro proceso de manejo de errores, tenemos dos casos:

  1. Para errores generales como no encontrado, o parámetros no válidos que se pasan a una acción, devolvemos a HttpResponseExceptionpara detener el procesamiento de inmediato. Además, para errores de modelo en nuestras acciones, entregaremos el diccionario de estado del modelo a la Request.CreateErrorResponseextensión y lo envolveremos en a HttpResponseException. Agregar el diccionario de estado del modelo da como resultado una lista de los errores del modelo enviados en el cuerpo de la respuesta.

  2. Para los errores que se producen en capas superiores, errores del servidor, dejamos que la excepción aparezca en la aplicación API web, aquí tenemos un filtro de excepción global que analiza la excepción, la registra con ELMAH e intenta que tenga sentido establecer la HTTP correcta código de estado y un mensaje de error amigable relevante como el cuerpo nuevamente en a HttpResponseException. Para las excepciones que no esperamos que el cliente reciba el error predeterminado del servidor interno 500, sino un mensaje genérico debido a razones de seguridad.

Actualización 3

Recientemente, después de elegir Web API 2, para enviar errores generales, ahora usamos la interfaz IHttpActionResult , específicamente las clases integradas para en el System.Web.Http.Resultsespacio de nombres como NotFound, BadRequest cuando encajan, si no es así, las extendemos, por ejemplo Un resultado NotFound con un mensaje de respuesta:

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);
    }
}
pib
fuente
Gracias por tu respuesta geepie, es una buena experiencia, ¿así que prefieres enviar expcetion inmediatamente?
cuongle
Como dije, realmente depende de la excepción. Una excepción grave como, por ejemplo, que el usuario pasa a la API web un parámetro no válido a un punto final, luego crearía una HttpResponseException y la devolvería directamente a la aplicación consumidora.
pib
Las excepciones en la pregunta son realmente más sobre validación, consulte stackoverflow.com/a/22163675/200442 .
Daniel Little
1
@DanielLittle Vuelve a leer su pregunta. Cito: "Este es solo un código de muestra, no importa ni los errores de validación ni los errores del servidor"
pib
@gdp Aun así, hay dos componentes, validación y excepciones, por lo que es mejor cubrir ambos.
Daniel Little
185

ASP.NET Web API 2 realmente lo simplificó. Por ejemplo, el siguiente código:

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

devuelve el siguiente contenido al navegador cuando no se encuentra el elemento:

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

Sugerencia: No arroje HTTP Error 500 a menos que haya un error catastrófico (por ejemplo, WCF Fault Exception). Elija un código de estado HTTP apropiado que represente el estado de sus datos. (Vea el enlace de apigee a continuación).

Enlaces:

Manish Jain
fuente
44
Iría un paso más allá y arrojaría una ResourceNotFoundException desde el DAL / Repo que verifico en la Web Api 2.2 ExceptionHandler para Type ResourceNotFoundException y luego devuelvo "Producto con id xxx no encontrado". De esta manera, está anclado genéricamente en la arquitectura en lugar de cada acción.
Pascal
1
¿Hay algún uso específico para return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); Cuál es la diferencia entre CreateResponsey?CreateErrorResponse
Zapnologica
11
Según, w3.org/Protocols/rfc2616/rfc2616-sec10.html , un error del cliente es un código de nivel 400 y un error del servidor es un código de nivel 500. Por lo tanto, un código de error 500 puede ser muy apropiado en muchos casos para una API web, no solo errores "catastróficos".
Jess
2
Es necesario using System.Net.Http;para el CreateResponse()método de extensión en aparecer.
Adam Szabo
Lo que no me gusta de usar Request.CreateResponse () es que devuelve información innecesaria de serialización específica de Microsoft como "<string xmlns =" schemas.microsoft.com/2003/10/Serialization/ "> Mi error aquí </ string > ". Para situaciones en las que el estado 400 es apropiado, descubrí que ApiController.BadRequest (mensaje de cadena) devuelve una cadena "<Error> <Message> My error here </Message> </Error>". Pero no puedo encontrar su equivalente para devolver el estado 500 con un mensaje simple.
vkelman
76

Parece que tienes más problemas con la validación que con los errores / excepciones, así que diré un poco sobre ambos.

Validación

Las acciones del controlador generalmente deben tomar Modelos de entrada donde la validación se declara directamente en el modelo.

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

Luego puede usar un ActionFilterque envía automáticamente mensajes de validación al cliente.

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

Para obtener más información sobre esto, visite http://ben.onfabrik.com/posts/automatic-modelstate-validation-in-aspnet-mvc

Manejo de errores

Es mejor devolver un mensaje al cliente que represente la excepción que sucedió (con el código de estado relevante).

Fuera de la caja que debe usar Request.CreateErrorResponse(HttpStatusCode, message)si desea especificar un mensaje. Sin embargo, esto vincula el código al Requestobjeto, lo que no debería necesitar hacer.

Por lo general, creo mi propio tipo de excepción "segura" que espero que el cliente sepa cómo manejar y envolver a todos los demás con un error genérico 500.

Usar un filtro de acción para manejar las excepciones se vería así:

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

Entonces puedes registrarlo globalmente.

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

Este es mi tipo de excepción personalizado.

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

Una excepción de ejemplo que mi API puede lanzar.

public class NotAuthenticatedException : ApiException
{
    public NotAuthenticatedException()
        : base(HttpStatusCode.Forbidden)
    {
    }
}
Daniel Little
fuente
Tengo un problema relacionado con la respuesta de manejo de errores en la definición de clase ApiExceptionFilterAttribute. En el método OnException, la excepción. El código de estado no es válido ya que la excepción es una excepción web. ¿Qué puedo hacer en este caso?
razp26
1
@ razp26 si te refieres a que var exception = context.Exception as WebException;eso fue un error tipográfico, debería haber sidoApiException
Daniel Little
2
¿Puede agregar un ejemplo de cómo se usaría la clase ApiExceptionFilterAttribute?
razp26
36

Puedes lanzar una HttpResponseException

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

Para Web API 2, mis métodos devuelven constantemente IHttpActionResult, así que uso ...

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

    return ResponseMessage(
        Request.CreateResponse(
            HttpStatusCode.BadRequest, 
            validationErrors));
}
Mick
fuente
Esta respuesta está bien, aunque debería agregar una referencia aSystem.Net.Http
Bellash
20

Si está utilizando ASP.NET Web API 2, la forma más fácil es utilizar el método abreviado ApiController. Esto dará como resultado un BadRequestResult.

return BadRequest("message");
Fabian von Ellerts
fuente
Estrictamente para los errores de validación del modelo, uso la sobrecarga de BadRequest () que acepta el objeto ModelState:return BadRequest(ModelState);
timmi4sa
4

puede usar ActionFilter personalizado en Web Api para validar el modelo

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

}

Registre la clase CustomAttribute en webApiConfig.cs config.Filters.Add (new DRFValidationFilters ());

LokeshChikkala
fuente
4

A partir Manish Jainde la respuesta (que está diseñada para Web API 2 que simplifica las cosas):

1) Use estructuras de validación para responder tantos errores de validación como sea posible. Estas estructuras también se pueden utilizar para responder a solicitudes provenientes de formularios.

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 capa de servicio devolverá ValidationResults, independientemente de que la operación sea exitosa o no. P.ej:

    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 Controller construirá la respuesta en función del resultado de la función de servicio

Una opción es colocar virtualmente todos los parámetros como opcionales y realizar una validación personalizada que devuelva una respuesta más significativa. Además, me aseguro de no permitir que ninguna excepción vaya más allá del límite del servicio.

    [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
fuente
3

Utilice el método incorporado "InternalServerError" (disponible en ApiController):

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

Solo para actualizar el estado actual de ASP.NET WebAPI. Ahora se llama a la interfaz IActionResulty la implementación no ha cambiado mucho:

[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
fuente
Esto parece interesante, pero ¿dónde se ubica este código específicamente en el proyecto? Estoy haciendo mi proyecto web api 2 en vb.net.
Off The Gold
Es solo un modelo para devolver el error y puede residir en cualquier lugar. Volvería una nueva instancia de la clase anterior en su controlador. Pero para ser honesto, trato de usar las clases integradas siempre que sea posible: this.Ok (), CreatedAtRoute (), NotFound (). La firma del método sería IHttpActionResult. No sé si cambiaron todo esto con NetCore
Thomas Hagström
-2

Para aquellos errores en los que modelstate.isvalid es falso, generalmente envío el error cuando lo arroja el código. Es fácil de entender para el desarrollador que está consumiendo mi servicio. En general, envío el resultado usando el código a continuación.

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

Esto envía el error al cliente en el siguiente formato, que es básicamente una lista de errores:

    [  
    "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
fuente
No recomendaría devolver este nivel de detalle en la excepción si se tratara de una API externa (es decir, expuesta a Internet pública). Debería trabajar un poco más en el filtro y enviar un objeto JSON (o XML si ese es el formato elegido) detallando el error en lugar de solo una ToString de excepción.
Sudhanshu Mishra
Correcto no envió esta excepción para API externa. Pero puede usarlo para depurar problemas en la API interna y durante las pruebas.
Ashish Sahu