Najlepsze praktyki zwracania błędów w ASP.NET Web API

Mam obawy co do sposobu, w jaki zwracamy błędy klientowi.

Czy zwracamy błąd natychmiast rzucając HttpResponseException gdy otrzymamy błąd:

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

Lub gromadzimy wszystkie błędy i odsyłamy do klienta:

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

To tylko przykładowy kod, nie ma znaczenia ani błędy walidacji, ani błąd serwera, po prostu chciałbym poznać najlepszą praktykę, wady i zalety każdego podejścia.

Author: Guido Leenders, 2012-05-24

11 answers

Dla mnie zazwyczaj odsyłam HttpResponseException i ustawiam kod statusu odpowiednio w zależności od rzuconego wyjątku i czy wyjątek jest fatalny czy nie, zadecyduje czy odeślę HttpResponseException natychmiast.

Na koniec dnia jego API odsyła odpowiedzi, a nie widoki, więc myślę, że jej dobrze, aby wysłać wiadomość z wyjątkiem i kodu stanu do konsumenta. Obecnie nie musiałem gromadzić błędów i odsyłać ich z powrotem, ponieważ większość wyjątków jest zwykle spowodowana nieprawidłowym parametry lub wywołania itp.

Przykładem w mojej aplikacji jest to, że czasami klient poprosi o dane, ale nie ma żadnych danych dostępnych, więc rzucam Niestandardowy noDataAvailableException i niech bańka do aplikacji web api, gdzie następnie w moim niestandardowym filtrze, który przechwytuje go wysyłając odpowiednią wiadomość wraz z poprawnym kodem stanu.

Nie jestem w 100% pewien, jaka jest najlepsza praktyka w tym zakresie, ale to działa dla mnie obecnie, więc to, co im robiąc.

Update :

Odkąd odpowiedziałem na to pytanie, na ten temat napisano kilka postów na blogu:

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

(ten ma kilka nowych funkcji w nocnych kompilacjach) http://blogs.msdn.com/b/youssefm/archive/2012/06/28/error-handling-in-asp-net-webapi.aspx

Update 2

Aktualizacja do naszego procesu obsługi błędów, mamy dwa przypadki:

  1. W przypadku ogólnych błędów, takich jak not found lub nieprawidłowe parametry przekazywane do akcji zwracamy wywołanie HttpResponseException, aby natychmiast zatrzymać przetwarzanie. Dodatkowo w przypadku błędów modelu w naszych działaniach przekażemy słownik stanu modelu do rozszerzenia Request.CreateErrorResponse i zawiniemy go w HttpResponseException. Dodanie słownika stanu modelu skutkuje listą błędów modelu wysłanych w treści odpowiedzi.

  2. Za błędy, które występują w wyższych warstwy, błędy serwera, pozwalamy Bubble wyjątku do aplikacji Web API, tutaj mamy globalny filtr WYJĄTKÓW, który patrzy na wyjątek, loguje go za pomocą elmah i trys, aby zrozumieć, że ustawia prawidłowy kod statusu http i odpowiedni przyjazny komunikat o błędzie jako ciało ponownie w HttpResponseException. W przypadku WYJĄTKÓW, których się nie spodziewamy, Klient otrzyma domyślny wewnętrzny błąd serwera 500, ale ogólny komunikat ze względów bezpieczeństwa.

Aktualizacja 3

Ostatnio, po odebraniu Web API 2, do odsyłania ogólnych błędów używamy interfejsu IHttpActionResult, a konkretnie wbudowanych klas w systemie.Www.Http.Przestrzenie nazw wyników, takie jak NotFound, BadRequest, gdy pasują, jeśli nie, rozszerzamy je, na przykład wynik notfound z Komunikatem odpowiedzi:

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
Author: gdp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-10 16:44:47

ASP.NET Web API 2 naprawdę to uprościło. Na przykład następujący kod:

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

Zwraca następującą zawartość do przeglądarki, gdy element nie zostanie znaleziony:

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

Sugestia: nie wyrzucaj błędu HTTP 500, chyba że wystąpi katastrofalny błąd (na przykład wyjątek błędu WCF). Wybierz odpowiedni kod statusu HTTP, który reprezentuje stan Twoich danych. (Zobacz link apigee poniżej.)

Linki:

 150
Author: Manish Jain,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-22 21:56:01

Wygląda na to, że masz więcej problemów z walidacją niż błędy/wyjątki, więc powiem trochę o obu.

Walidacja

Działania kontrolera powinny zasadniczo przyjmować modele wejściowe, w których Walidacja jest deklarowana bezpośrednio w modelu.

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

Następnie możesz użyć ActionFilter, który automatycznie wysyła wiadomości walidacyjne z powrotem do klienta.

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

Aby uzyskać więcej informacji o tym sprawdź http://ben.onfabrik.com/posts/automatic-modelstate-validation-in-aspnet-mvc

Obsługa błędów

Najlepiej jest zwrócić klientowi wiadomość, która reprezentuje zaistniały wyjątek (z odpowiednim kodem stanu).

Po wyjęciu z pudełka musisz użyć Request.CreateErrorResponse(HttpStatusCode, message), jeśli chcesz podać wiadomość. Wiąże to jednak kod z obiektem Request, czego nie trzeba robić.

Zwykle tworzę swój własny typ" bezpiecznego " wyjątku, który spodziewaj się, że klient będzie wiedział, jak obsługiwać i owijać wszystkie inne z ogólnym błędem 500.

Użycie filtra akcji do obsługi wyjątków wyglądałoby tak:

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

Następnie możesz zarejestrować go globalnie.

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

To jest mój niestandardowy typ wyjątku.

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

Przykładowy wyjątek, który może rzucić moje API.

public class NotAuthenticatedException : ApiException
{
    public NotAuthenticatedException()
        : base(HttpStatusCode.Forbidden)
    {
    }
}
 61
Author: Daniel Little,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-23 04:06:27

Możesz rzucić HttpResponseException

HttpResponseMessage response = 
    this.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "your message");
throw new HttpResponseException(response);
 29
Author: tartakynov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-03-04 05:15:18

Dla Web API 2 moje metody konsekwentnie zwracają IHttpActionResult więc używam...

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

    return ResponseMessage(
        Request.CreateResponse(
            HttpStatusCode.BadRequest, 
            validationErrors));
}
 14
Author: Mick,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-03 23:49:45

Możesz użyć niestandardowego filtra ActionFilter w Web Api do walidacji modelu

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

}

Zarejestruj klasę CustomAttribute w webApiConfig.cs config.Filtry.Add (new DRFValidationFilters ());

 4
Author: LokeshChikkala,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-03-18 09:50:23

W związku z tym, że nie jest to możliwe, nie jest to możliwe.]}

1) użyj struktur walidacji , aby odpowiedzieć na jak najwięcej błędów walidacji. Struktury te mogą być również wykorzystywane do odpowiedzi na wnioski płynące z formularzy.

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) warstwa usług zwróci ValidationResult s, niezależnie od powodzenia operacji. Np.:

    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) Kontroler API skonstruuje odpowiedź na podstawie funkcji usługi wynik

Jedną z opcji jest umieszczenie praktycznie wszystkich parametrów jako opcjonalnych i przeprowadzenie walidacji niestandardowej, która zwróci bardziej znaczącą odpowiedź. Dbam również o to, aby żaden wyjątek nie wykraczał poza granice służb.

    [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
Author: Alexei,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-19 14:32:01

Użyj wbudowanej metody "InternalServerError" (dostępnej w ApiController):

return InternalServerError();
//or...
return InternalServerError(new YourException("your message"));
 1
Author: Rusty,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-04 15:03:14

Jeśli używasz ASP.NET Web API 2, najprostszym sposobem jest użycie krótkiej metody ApiController. Spowoduje to złe żądanie rezultatu.

return BadRequest("message");
 1
Author: Fabian von Ellerts,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-22 12:22:48

Aby zaktualizować aktualny stan ASP.NET WebAPI. Interfejs jest teraz nazywany IActionResult i implementacja niewiele się zmieniła:

[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
Author: Thomas Hagström,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-05-10 07:51:29

Dla tych błędów, gdzie modelstate.isvalid jest false, generalnie wysyłam błąd, ponieważ jest wyrzucany przez kod. Jest łatwy do zrozumienia dla programisty, który konsumuje moje usługi. Zazwyczaj wysyłam wynik za pomocą poniższego kodu.

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

To wysyła błąd do Klienta w poniższym formacie, który jest w zasadzie listą błędów:

    [  
    "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
Author: Ashish Sahu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-19 23:53:04