Лучшая практика для возврата ошибок в ASP.NET Web API

385

У меня есть опасения по поводу того, как мы возвращаем ошибки клиенту.

Должны ли мы немедленно возвращать ошибку, выбрасывая 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);
}

Это просто пример кода, не имеет значения ни ошибки валидации, ни ошибки сервера, я просто хотел бы узнать лучшие практики, плюсы и минусы каждого подхода.

cuongle
источник
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и отправляю его в приложение Web 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. Для общих ошибок, таких как не найденные или неверные параметры, переданные действию, мы возвращаем a, HttpResponseExceptionчтобы немедленно остановить обработку. Кроме того, для ошибок модели в наших действиях мы передадим словарь состояния модели Request.CreateErrorResponseрасширению и поместим его в HttpResponseException. Добавление словаря состояния модели приводит к появлению списка ошибок модели, отправленных в теле ответа.

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

Обновление 3

Недавно, после получения Web API 2, для отправки общих ошибок мы теперь используем интерфейс IHttpActionResult , в частности, встроенные классы для в System.Web.Http.Resultsпространстве имен, такие как NotFound, BadRequest, когда они подходят, если они не расширяют их, например, 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);
    }
}
ВВП
источник
Спасибо за ответ, Geepie, это хороший опыт, поэтому вы предпочитаете немедленно отправить expcetion?
cuongle
Как я уже сказал, это действительно зависит от исключения. Неустранимое исключение, такое как, например, то, что пользователь передает веб-интерфейсу API недопустимый параметр в конечную точку, затем я создаю исключение HttpResponseException и сразу же возвращаю его приложению-потребителю.
ВВП
Исключения в вопросе действительно больше о проверке см. Stackoverflow.com/a/22163675/200442 .
Даниэль Литтл
1
@DanielLittle Перечитайте его вопрос. Я цитирую: «Это просто пример кода, не имеет значения ни ошибки валидации, ни ошибки сервера»
gdp
@gdp Несмотря на это, на самом деле есть два компонента: проверка и исключения, поэтому лучше охватить оба.
Даниэль Литтл
185

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-ошибку 500, если нет катастрофической ошибки (например, исключение ошибки WCF). Выберите соответствующий код состояния HTTP, который представляет состояние ваших данных. (См. Ссылку на Apigee ниже.)

Ссылки:

Маниш Джайн
источник
4
Я бы сделал еще один шаг вперед и выдал бы исключение ResourceNotFoundException из DAL / Repo, которое я проверял в ExceptionHandler Web Api 2.2 для типа ResourceNotFoundException, а затем возвращал «Продукт с идентификатором xxx не найден». Таким образом, он обычно привязан к архитектуре, а не к каждому действию.
Паскаль
1
Есть ли использование конкретного для return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); Какова разница между CreateResponseиCreateErrorResponse
Zapnologica
11
Согласно 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 (строковое сообщение) возвращает более качественную строку "<Ошибка> <Сообщение> Моя ошибка здесь </ 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);
tartakynov
источник
23

Для Web API 2 мои методы последовательно возвращают IHttpActionResult, поэтому я использую ...

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

    return ResponseMessage(
        Request.CreateResponse(
            HttpStatusCode.BadRequest, 
            validationErrors));
}
Мик
источник
Этот ответ хорошо, в то время как вы должны добавить ссылку наSystem.Net.Http
Беллаш
20

Если вы используете 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 (new DRFValidationFilters ());

LokeshChikkala
источник
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.
Off Gold
Это просто модель для возврата ошибки и может находиться в любом месте. Вы бы вернули новый экземпляр вышеуказанного класса в вашем контроллере. Но, если честно, я стараюсь по возможности использовать встроенные классы: this.Ok (), CreatedAtRoute (), NotFound (). Подпись метода будет IHttpActionResult. Не знаю, изменили ли они все это с помощью NetCore
Томас Хагстрем
-2

Для тех ошибок, где modelstate.isvalid имеет значение false, я обычно отправляю ошибку, поскольку она генерируется кодом. Это легко понять разработчику, который использует мой сервис. Я обычно отправляю результат, используя приведенный ниже код.

     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 исключения.
Судханшу Мишра
Правильно, не отправил это исключение для внешнего API. Но вы можете использовать его для устранения проблем во внутреннем API и во время тестирования.
Ашиш Саху