Prática recomendada para retornar erros na API da Web do ASP.NET

384

Estou preocupado com a maneira como retornamos erros ao cliente.

Retornamos o erro imediatamente lançando HttpResponseException quando obtemos um erro:

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 acumulamos todos os erros e depois devolvemos ao 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 é apenas um código de amostra, não importa erros de validação ou erro do servidor, apenas gostaria de conhecer as melhores práticas, os prós e os contras de cada abordagem.

cuongle
fonte
11
Consulte stackoverflow.com/a/22163675/200442 que você deve usar ModelState.
Daniel Daniel
11
Observe que as respostas aqui abrangem apenas as exceções lançadas no próprio controlador. Se a sua API retorna um <modelo> IQueryable que não tenha sido executado ainda, a exceção não está no controle e não é pego ...
Jess
3
Very nice pergunta, mas de alguma forma eu não estou recebendo qualquer sobrecarga de construtor da HttpResponseExceptionclasse que tem dois parâmetros mencionados em seu post - HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest) ou sejaHttpResponseException(string, HttpStatusCode)
RBT

Respostas:

293

Normalmente, para mim, eu envio de volta um HttpResponseExceptione defino o código de status de acordo com a exceção lançada e, se a exceção for fatal ou não, determinará se o envio HttpResponseExceptionimediatamente.

No final do dia, é uma API que envia respostas e não visualizações, então acho que é bom enviar uma mensagem com o código de exceção e status para o consumidor. No momento, não preciso acumular erros e enviá-los de volta, pois a maioria das exceções geralmente ocorre devido a parâmetros ou chamadas incorretos, etc.

Um exemplo no meu aplicativo é que, às vezes, o cliente solicita dados, mas não há dados disponíveis, então eu ligo um NoDataAvailableException e deixo ele borbulhar no aplicativo API da Web, onde, em seguida, no meu filtro personalizado, que o captura enviando de volta um mensagem relevante junto com o código de status correto.

Não tenho 100% de certeza sobre qual é a melhor prática para isso, mas isso está funcionando para mim atualmente e é isso que estou fazendo.

Atualização :

Desde que eu respondi a essa pergunta, algumas postagens do blog foram escritas sobre o tópico:

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

(este possui alguns novos recursos nas compilações noturnas) https://docs.microsoft.com/archive/blogs/youssefm/error-handling-in-asp-net-webapi

Atualização 2

Atualize para o nosso processo de tratamento de erros, temos dois casos:

  1. Para erros gerais como não encontrados ou parâmetros inválidos sendo passados ​​para uma ação, retornamos a HttpResponseExceptionpara interromper o processamento imediatamente. Além disso, para erros de modelo em nossas ações, entregaremos o dicionário de estado do modelo na Request.CreateErrorResponseextensão e o envolveremos em um HttpResponseException. Adicionar o dicionário de estado do modelo resulta em uma lista dos erros de modelo enviados no corpo da resposta.

  2. Para erros que ocorrem em camadas mais altas, erros de servidor, deixamos o balão de exceção no aplicativo API da Web, aqui temos um filtro de exceção global que analisa a exceção, a registra no ELMAH e tenta entender a definição do HTTP correto código de status e uma mensagem de erro amigável relevante como o corpo novamente em a HttpResponseException. Para exceções que não esperamos, o cliente receberá o erro padrão do servidor interno 500, mas uma mensagem genérica devido a motivos de segurança.

Atualização 3

Recentemente, depois de escolher a API da Web 2, para enviar de volta erros gerais, agora usamos a interface IHttpActionResult , especificamente as classes incorporadas no System.Web.Http.Resultsespaço para nome, como NotFound, BadRequest quando cabem, se não o estendermos, por exemplo um resultado NotFound com uma mensagem de resposta:

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
fonte
Obrigado pela sua resposta, geepie, é uma boa experiência, então você prefere enviar a expulsão imediatamente?
cuongle
Como eu disse, isso realmente depende da exceção. Uma exceção fatal, como por exemplo, o usuário transmite à API da Web um parâmetro inválido para um nó de extremidade, então eu criaria uma HttpResponseException e a retornaria imediatamente para o aplicativo consumidor.
Gdp 31/05
As exceções na pergunta são realmente mais sobre validação, veja stackoverflow.com/a/22163675/200442 .
Daniel Little
11
@DanielLittle Releia sua pergunta. Eu cito: "Este é apenas um código de amostra, não importa erros de validação ou erro do servidor"
gdp 4/14
@gdp Mesmo assim, existem realmente dois componentes, validação e exceções, por isso é melhor cobrir os dois.
Daniel Little
184

A API da Web do ASP.NET 2 realmente o simplificou. Por exemplo, o seguinte 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);
    }
}

retorna o seguinte conteúdo para o navegador quando o item não for encontrado:

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

Sugestão: Não gere o Erro HTTP 500, a menos que haja um erro catastrófico (por exemplo, Exceção de falha do WCF). Escolha um código de status HTTP apropriado que represente o estado dos seus dados. (Veja o link do apigee abaixo.)

Ligações:

Manish Jain
fonte
4
Eu daria um passo adiante e lançaria uma ResourceNotFoundException do DAL / Repo, que verifico no Web Api 2.2 ExceptionHandler para o Tipo ResourceNotFoundException e, em seguida, retornei "Product with id xxx not found". Dessa forma, é genericamente ancorado na arquitetura, em vez de cada ação.
187 Pascal
11
Existe alguma utilização específica para o return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); que é a diferença entre CreateResponseeCreateErrorResponse
Zapnologica
10
De acordo com w3.org/Protocols/rfc2616/rfc2616-sec10.html , um erro do cliente é um código de nível 400 e um erro do servidor é um código de nível 500. Portanto, um código de erro 500 pode ser muito apropriado em muitos casos para uma API da Web, não apenas erros "catastróficos".
21415 Jess
2
Você precisa using System.Net.Http;que o CreateResponse()método de extensão seja exibido.
23615 Adam Szabo
O que eu não gosto em usar Request.CreateResponse () é que ele retorna informações desnecessárias de serialização específicas da Microsoft, como "<string xmlns =" schemas.microsoft.com/2003/10/Serialization/">My error here </ string > ". Para situações em que o status 400 é apropriado, descobri que ApiController.BadRequest (mensagem de sequência) retorna uma sequência "<Error> <Message> Meu erro aqui </Message> </Error>". Mas não consigo encontrar o equivalente para retornar o status 500 com uma mensagem simples.
22416 vkelman
76

Parece que você está tendo mais problemas com a Validação do que erros / exceções, então vou falar um pouco sobre ambos.

Validação

As ações do controlador geralmente devem usar modelos de entrada nos quais a validação é declarada diretamente no modelo.

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

Em seguida, você pode usar um ActionFilterque envia automaticamente mensagens de validação de volta ao 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 mais informações sobre isso, visite http://ben.onfabrik.com/posts/automatic-modelstate-validation-in-aspnet-mvc

Manipulação de erros

É melhor retornar uma mensagem ao cliente que represente a exceção que ocorreu (com código de status relevante).

Fora da caixa, você deve usar Request.CreateErrorResponse(HttpStatusCode, message)se desejar especificar uma mensagem. No entanto, isso vincula o código ao Requestobjeto, o que você não precisa fazer.

Normalmente, crio meu próprio tipo de exceção "segura" que espero que o cliente saiba como manipular e agrupar todas as outras com um erro genérico de 500.

O uso de um filtro de ação para lidar com as exceções seria assim:

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

Então você pode registrá-lo globalmente.

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

Este é o meu tipo de exceção 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; }
        }
    }
}

Uma exceção de exemplo que minha API pode lançar.

public class NotAuthenticatedException : ApiException
{
    public NotAuthenticatedException()
        : base(HttpStatusCode.Forbidden)
    {
    }
}
Daniel Little
fonte
Eu tenho um problema relacionado à resposta de manipulação de erro na definição de classe ApiExceptionFilterAttribute. No método OnException, a exceção.StatusCode não é válida, pois a exceção é uma WebException. O que posso fazer neste caso?
precisa saber é o seguinte
11
@ razp26 se você está se referindo ao tipo var exception = context.Exception as WebException;que foi um erro de digitação, deveria ter sido #ApiException
Daniel Little
2
Você pode adicionar um exemplo de como a classe ApiExceptionFilterAttribute seria usada?
precisa saber é o seguinte
36

Você pode lançar um HttpResponseException

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

Para a API da Web 2, meus métodos retornam consistentemente IHttpActionResult para que eu use ...

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

    return ResponseMessage(
        Request.CreateResponse(
            HttpStatusCode.BadRequest, 
            validationErrors));
}
Mick
fonte
Esta resposta está correta, enquanto você deve adicionar referência a #System.Net.Http
Bellash
19

Se você estiver usando a API da Web 2 do ASP.NET, a maneira mais fácil é usar o método curto ApiController. Isso resultará em um BadRequestResult.

return BadRequest("message");
Fabian von Ellerts
fonte
Estritamente para erros de validação de modelo que eu uso a sobrecarga de BadRequest () que aceita o objeto ModelState:return BadRequest(ModelState);
timmi4sa
4

você pode usar o ActionFilter personalizado na API da Web para validar o 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 a classe CustomAttribute em webApiConfig.cs config.Filters.Add (new DRFValidationFilters ());

LokeshChikkala
fonte
4

Desenvolvendo a Manish Jainresposta (que é destinada à API da Web 2, que simplifica as coisas):

1) Use estruturas de validação para responder ao maior número possível de erros de validação. Essas estruturas também podem ser usadas para responder a solicitações provenientes de formulários.

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) A camada de serviço retornará ValidationResults, independentemente da operação ter êxito ou não. Por exemplo:

    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) O API Controller construirá a resposta com base no resultado da função de serviço

Uma opção é colocar virtualmente todos os parâmetros como opcionais e executar a validação personalizada que retorna uma resposta mais significativa. Além disso, estou cuidando para não permitir que nenhuma exceção ultrapasse os limites do serviço.

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

Use o método "InternalServerError" incorporado (disponível no ApiController):

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

Apenas para atualizar o estado atual da ASP.NET WebAPI. A interface agora é chamada IActionResulte a implementação não mudou muito:

[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
fonte
Isso parece interessante, mas onde especificamente no projeto esse código é colocado? Estou fazendo o meu projeto da web api 2 em vb.net.
Off The Gold
É apenas um modelo para retornar o erro e pode residir em qualquer lugar. Você retornaria uma nova instância da classe acima em seu Controller. Mas, para ser sincero, tento usar as classes incorporadas sempre que possível: this.Ok (), CreatedAtRoute (), NotFound (). A assinatura do método seria IHttpActionResult. Não sei se eles mudaram tudo isso com Netcore
Thomas Hagström
-2

Para os erros em que modelstate.isvalid é falso, geralmente envio o erro conforme ele é gerado pelo código. É fácil de entender para o desenvolvedor que está consumindo meu serviço. Eu geralmente envio o resultado usando o código abaixo.

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

Isso envia o erro ao cliente no formato abaixo, que é basicamente uma lista de erros:

    [  
    "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
fonte
Eu não recomendaria enviar de volta esse nível de detalhe na exceção se fosse uma API externa (ex. Exposta à Internet pública). Você deve trabalhar um pouco mais no filtro e enviar de volta um objeto JSON (ou XML, se esse for o formato escolhido) detalhando o erro, em vez de apenas uma exceção ToString.
Sudhanshu Mishra 31/08/16
Correto não enviou essa exceção para API externa. Mas você pode usá-lo para depurar problemas na API interna e durante o teste.
Ashish Sahu