Найкраща практика повернення помилок у веб-API ASP.NET


384

У мене є стурбованість тим, як ми повертаємо помилки клієнту.

Чи повертаємо ми помилку негайно, кидаючи HttpResponseException, коли ми отримуємо помилку:

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

Або ми накопичуємо всі помилки, а потім відправляємо назад клієнту:

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

Це лише зразок коду, він не має значення ні помилок перевірки, ні помилки сервера, я просто хотів би знати найкращу практику, плюси і мінуси кожного підходу.


1
Дивіться stackoverflow.com/a/22163675/200442, який ви повинні використовувати ModelState.
Даніель Малий

1
Зауважте, що відповіді тут охоплюють лише винятки, які кидаються в сам контролер. Якщо ваш API повертає IQueryable <Model>, який ще не був виконаний, виняток не знаходиться в контролері і не вловлюється ...
Jess,

3
Дуже приємне запитання, але чомусь у мене не HttpResponseExceptionвиникає перевантаженості класу конструктором, який приймає два параметри, згадані у вашому дописі - HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest) тобтоHttpResponseException(string, HttpStatusCode)
RBT

Відповіді:


293

Для мене я зазвичай надсилаю назад HttpResponseExceptionі встановлюю відповідно код статусу залежно від викинутого винятку, і якщо виняток є фатальним чи ні, я визначаю, чи надсилаю його HttpResponseExceptionнегайно.

Зрештою, це API, який надсилає відповіді, а не перегляди, тому я вважаю, що добре відправити споживачеві повідомлення з винятком та кодом статусу. Наразі мені не потрібно накопичувати помилки та надсилати їх назад, оскільки більшість винятків зазвичай пов’язані з неправильними параметрами або дзвінками тощо.

Прикладом мого додатка є те, що іноді клієнт запитає дані, але даних немає, тому я кидаю користувальницькі дані NoDataAvailableException та пускаю його у додаток для веб-додатків API, де потім у мій користувацький фільтр, який захоплює його, відсилаючи назад відповідне повідомлення разом з правильним кодом статусу.

Я не на 100% впевнений у тому, яка найкраща практика для цього є, але зараз це працює для мене, тому я це роблю.

Оновлення :

Оскільки я відповів на це запитання, на цю тему написано кілька публікацій у блозі:

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

(у цього є деякі нові функції у нічних складах) https://docs.microsoft.com/archive/blogs/youssefm/error-handling-in-asp-net-webapi

Оновлення 2

Оновіть наш процес обробки помилок, ми маємо два випадки:

  1. Для загальних помилок, таких як не знайдені, або недійсних параметрів, що передаються до дії, ми повертаємо а HttpResponseExceptionдля припинення обробки негайно. Додатково для помилок моделі в наших діях ми передамо словник стану моделі в Request.CreateErrorResponseрозширення і загорнемо його в HttpResponseException. Додавання словника стану моделі приводить до списку помилок моделі, надісланих в тілі відповідей.

  2. Для помилок, які трапляються у вищих шарах, помилок сервера, ми пускаємо міхур виключення до програми Web API, тут ми маємо глобальний фільтр винятків, який дивиться на виняток, записує його в ELMAH і намагається зрозуміти, встановивши правильний HTTP код статусу та відповідне дружнє повідомлення про помилку, як тіло знову в а HttpResponseException. За винятками, що ми не очікуємо, що клієнт отримає 500 внутрішніх помилок сервера, але загальне повідомлення з міркувань безпеки.

Оновлення 3

Останнім часом, після вибору Web API 2, для повернення загальних помилок ми тепер використовуємо інтерфейс IHttpActionResult , зокрема вбудовані класи для System.Web.Http.Resultsпростору імен, такі як NotFound, BadRequest, коли вони підходять, якщо вони не розширюють їх, наприклад a NotFound результат із відповідним повідомленням:

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

Дякую за відповідь, джипі, це хороший досвід, тому ви віддаєте перевагу негайно відправити експлікацію?
cuongle

Як я сказав, це дійсно залежить від винятку. Фатальний виняток, такий як, наприклад, користувач передає Web Api недійсний параметр кінцевій точці, тоді я створив би HttpResponseException і повертав його відразу до споживаючої програми.
gdp

Винятки з питання справді більше стосуються перевірки, див. Stackoverflow.com/a/22163675/200442 .
Даніель Літтл

1
@DanielLittle Перечитайте його питання. Я цитую: "Це просто зразок коду, це не має значення ні помилок перевірки, ні помилки сервера"
gdp

@gdp Навіть для цього дійсно є два компоненти, перевірка та винятки, тому краще охопити обидва.
Даніель Літтл

184

ASP.NET Web API 2 дійсно спростив його. Наприклад, наступний код:

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

повертає наступний вміст у браузер, коли елемент не знайдено:

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

Пропозиція: Не кидайте HTTP Error 500, якщо не сталася катастрофічна помилка (наприклад, Виняток помилок WCF). Виберіть відповідний код статусу HTTP, який відображає стан ваших даних. (Дивіться посилання на apigee нижче.)

Посилання:


4
Я б пішов на крок далі і кинув ResourceNotFoundException з DAL / Repo, який я перевіряю в Web Api 2.2 ExceptionHandler для типу ResourceNotFoundException, а потім повертаю "Product with id xxx not found". Таким чином, він загалом закріпився в архітектурі замість кожної дії.
Паскаль

1
Чи є якесь конкретне використання для return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); чого в чому різниця CreateResponseтаCreateErrorResponse
Zapnologica

10
Відповідно, w3.org/Protocols/rfc2616/rfc2616-sec10.html , помилка клієнта - код рівня 400, а помилка сервера - код рівня 500. Тож код помилки 500 може бути дуже підходящим у багатьох випадках для веб-API, а не лише "катастрофічних" помилок.
Джесс

2
Вам потрібно using System.Net.Http;для CreateResponse()методу розширення , щоб показати.
Адам Сабо

Що мені не подобається у використанні Request.CreateResponse (), це те, що він повертає непотрібну інформацію про серіалізацію для Microsoft, наприклад "<string xmlns =" schemas.microsoft.com/2003/10/Serialization / "> Моя помилка тут </string> > ". У ситуаціях, коли стан відповідає 400, я виявив, що ApiController.BadRequest (рядкове повідомлення) повертає кращу "<Error> <Message> Мою помилку </Message> </Error>". Але я не можу знайти його еквівалент для повернення статусу 500 простим повідомленням.
vkelman

76

Схоже, у вас більше проблем з валідацією, ніж помилок / винятків, тому я скажу трохи про обидва.

Перевірка

Дії контролера, як правило, повинні приймати вхідні моделі, де перевірка оголошується безпосередньо на моделі.

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

Тоді ви можете використовувати повідомлення, ActionFilterяке автоматично надсилає клієнтові повідомлення назад.

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

Для отримання додаткової інформації про це відвідайте http://ben.onfabrik.com/posts/automatic-modelstate-validation-in-aspnet-mvc

Помилка обробки

Найкраще повернути повідомлення клієнту, який представляє виняток, що стався (з відповідним кодом статусу).

Поле, яке ви повинні використовувати, Request.CreateErrorResponse(HttpStatusCode, message)якщо ви хочете вказати повідомлення. Однак це пов'язує код з Requestоб'єктом, що вам не потрібно робити.

Я, як правило, створюю власний тип "безпечного" винятку, і, напевно, клієнт знатиме, як обробляти та обробляти всіх інших із загальною помилкою 500.

Використання фільтра дій для обробки винятків виглядатиме так:

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

Тоді ви можете зареєструвати це у всьому світі.

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

Це мій типовий виняток.

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

Приклад виключення, яке мій API може кинути.

public class NotAuthenticatedException : ApiException
{
    public NotAuthenticatedException()
        : base(HttpStatusCode.Forbidden)
    {
    }
}

У мене є проблема, пов'язана з відповіддю на обробку помилок у визначенні класу ApiExceptionFilterAttribute. У методі OnException, виключення.StatusCode недійсне, оскільки виняток є WebException. Що я можу зробити в цьому випадку?
razp26

1
@ razp26, якщо ви посилаєтесь на подібне, var exception = context.Exception as WebException;що було друком, це повинно було бутиApiException
Даніель Літтл

2
Чи можете ви додати приклад того, як буде використовуватися клас ApiExceptionFilterAttribute?
razp26

36

Ви можете кинути HttpResponseException

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

23

Для Web API 2 мої методи послідовно повертають IHttpActionResult, тому я використовую ...

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

    return ResponseMessage(
        Request.CreateResponse(
            HttpStatusCode.BadRequest, 
            validationErrors));
}

Ця відповідь гаразд, тоді як вам слід додати посилання наSystem.Net.Http
Bellash

19

Якщо ви використовуєте ASP.NET Web API 2, найпростішим способом є використання короткого методу ApiController. Це призведе до BadRequestResult.

return BadRequest("message");

Суворо для помилок перевірки моделі я використовую перевантаження BadRequest (), який приймає об'єкт ModelState:return BadRequest(ModelState);
timmi4sa

4

ви можете використовувати спеціальний ActionFilter в Web Api для перевірки моделі

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

}

Зареєструйте клас CustomAttribute в webApiConfig.cs config.Filters.Add (новий DRFValidationFilters ());


4

Надбудова відповіді на Manish Jainвідповідь (що призначено для Web API 2, який спрощує речі):

1) Використовуйте структури перевірки, щоб відповісти на якомога більше помилок перевірки. Ці структури також можуть бути використані для відповіді на запити, що надходять із форм.

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) Службовий рівень поверне ValidationResults, незалежно від того, успішна чи ні операція. Наприклад:

    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 побудує відповідь на основі результату сервісної функції

Один із варіантів - поставити практично всі параметри як необов'язкові та виконати спеціальну перевірку, яка повертає більш значущу відповідь. Також я подбаю про те, щоб не допустити жодного винятку за межі службової межі.

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

3

Використовуйте вбудований метод "InternalServerError" (доступний в ApiController):

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

0

Просто для оновлення поточного стану ASP.NET WebAPI. Зараз інтерфейс викликається, IActionResultа реалізація сильно не змінилася:

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

Це виглядає цікаво, але де конкретно в проекті розміщений цей код? Я роблю свій веб-проект api 2 у vb.net.
Вимкнено золото

Це просто модель повернення помилки і може перебувати в будь-якому місці. Ви повернете новий екземпляр вищевказаного класу у свій контролер. Але якщо чесно, я намагаюся використовувати вбудовані в класи, коли це можливо: this.Ok (), CreatedAtRoute (), NotFound (). Підписом методу буде IHttpActionResult. Не знаю, чи змінили вони все це з NetCore
Thomas Hagström

-2

Для тих помилок, де modelstate.isvalid є помилковим, я зазвичай надсилаю помилку, оскільки її кидає код. Це легко зрозуміти для розробника, який споживає мою послугу. Я зазвичай надсилаю результат за допомогою наведеного нижче коду.

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

Це посилає клієнту помилку у форматі нижче, що в основному є списком помилок:

    [  
    "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)"
    ]

Я б не рекомендував надсилати цей рівень деталізації за винятком, якби це був зовнішній API (тобто піддавався публічному Інтернету). Вам слід виконати ще якусь роботу у фільтрі та відправити назад об’єкт JSON (або XML, якщо це обраний формат), де деталізується помилка, а не просто ToString винятку.
Sudhanshu Mishra

Правильний Dont надіслав цей виняток для зовнішнього API. Але ви можете використовувати його для налагодження проблем у внутрішньому API та під час тестування.
Ашиш Саху
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.