Вопрос по rest, c# – Лучшая практика для возврата ошибок в ASP.NET Web API

326

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

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

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

Очень хороший вопрос, но почему-то я не получаю никакой перегрузки конструктораHttpResponseException класс, который принимает два параметра, упомянутых в вашем посте -HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest)  то естьHttpResponseException(string, HttpStatusCode) RBT
Обратите внимание, что ответы здесь охватывают только исключения, которые выбрасываются в самом контроллере. Если ваш API возвращает IQueryable & lt; Model & gt; это еще не было выполнено, исключение не находится в контроллере и не перехватывается ... Jess
Увидетьstackoverflow.com/a/22163675/200442 вы должны использоватьModelState. Daniel Little

Ваш Ответ

11   ответов
34

Вы можете бросить HttpResponseException

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

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

1) Использованиеvalidation structures ответить как можно больше ошибок валидации. Эти структуры также могут использоваться для ответа на запросы, поступающие из форм.

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) Service layer вернусь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 Controller построит ответ на основе результата сервисной функции

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

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

you can use custom ActionFilter in Web Api to validate model

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

2

Используйте встроенный & quot; InternalServerError & quot; метод (доступен в ApiController):

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

Если вы используете ASP.NET Web API 2, самый простой способ - использовать короткий метод ApiController. Это приведет к BadRequestResult.

return BadRequest("message");
19

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

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

    return ResponseMessage(
        Request.CreateResponse(
            HttpStatusCode.BadRequest, 
            validationErrors));
}
Этот ответ в порядке, в то время как вы должны добавить ссылку наSystem.Net.Http
-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. Но вы можете использовать его для устранения проблем во внутреннем API и во время тестирования.
Я бы не рекомендовал возвращать этот уровень детализации, за исключением случаев, когда это был внешний API (т. Е. Общедоступный Интернет). Вы должны проделать дополнительную работу с фильтром и отправить обратно объект JSON (или XML, если это выбранный формат) с подробным описанием ошибки, а не просто ToString исключения.
161

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

Ссылки:

Есть ли конкретное применение дляreturn Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);  В чем разница междуCreateResponse а такжеCreateErrorResponse
В соответствии с,w3.org/Protocols/rfc2616/rfc2616-sec10.htmlошибка клиента - это код уровня 400, а ошибка сервера - код уровня 500. Таким образом, код ошибки 500 может быть очень уместным во многих случаях для веб-API, а не просто «катастрофический». ошибки.
Я хотел бы пойти еще дальше и выбросить ResourceNotFoundException из DAL / Repo, который я проверяю в Web Api 2.2 ExceptionHandler для типа ResourceNotFoundException, а затем я возвращаю «Продукт с идентификатором xxx не найден». Таким образом, он обычно привязан к архитектуре, а не к каждому действию.
Тебе нужноusing System.Net.Http; дляCreateResponse() метод расширения, чтобы показать.
Что мне не нравится в использовании Request.CreateResponse (), так это то, что он возвращает ненужную специфическую для Microsoft информацию сериализации, например & lt; string xmlns = & quot;schemas.microsoft.com/2003/10/Serialization/">My ошибка здесь & lt; / string & gt; & quot ;. Для ситуаций, когда статус 400 подходит, я обнаружил, что ApiController.BadRequest (строковое сообщение) возвращает более подходящее сообщение & lt; Error & lt; Моя ошибка здесь & lt; / Message & gt; / Error & gt; & quot; строка. Но я не могу найти эквивалент для возврата статуса 500 с простым сообщением.
68

Похоже, что у вас больше проблем с проверкой, чем с ошибками / исключениями, поэтому я немного расскажу об обоих.

Validation

Действия контроллера, как правило, должны принимать входные модели, где валидация объявляется непосредственно в модели.

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

Error handling

Лучше всего возвращать клиенту сообщение, которое представляет исключение, которое произошло (с соответствующим кодом состояния).

Из коробки вы должны использовать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)
    {
    }
}
@ razp26, если вы ссылаетесь на подобноеvar exception = context.Exception as WebException; это была опечатка, это должно было бытьApiException
У меня есть проблема, связанная с ответом на ошибку при определении класса ApiExceptionFilterAttribute. В методе OnException исключение. StatusCode недопустимо, поскольку исключение является WebException. Что я могу сделать в этом случае?
Можете ли вы добавить пример того, как будет использоваться класс ApiExceptionFilterAttribute?
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
254

Для меня я обычно отправляю обратноHttpResponseException и установите код состояния соответственно в зависимости от выданного исключения, и если исключение является фатальным или нет, определит, отправлю ли я обратноHttpResponseException немедленно.

В конце концов, это API, который отправляет ответы, а не просмотры, поэтому я думаю, что можно отправлять сообщение с кодом исключения и кодом потребителю. В настоящее время мне не нужно накапливать ошибки и отправлять их обратно, так как большинство исключений обычно связано с неправильными параметрами или вызовами и т. Д.

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

Я не уверен на 100% в том, что лучше для этого, но сейчас это работает для меня, вот что я делаю.

Update:

Поскольку я ответил на этот вопрос, на эту тему было написано несколько постов в блоге:

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

(у этого есть некоторые новые функции в ночных сборках) http://blogs.msdn.com/b/youssefm/archive/2012/06/28/error-handling-in-asp-net-webapi.aspx

Update 2

Обновление нашего процесса обработки ошибок, у нас есть два случая:

  1. For general errors like not found, or invalid parameters being passed to an action we return a HttpResponseException to stop processing immediately. Additionally for model errors in our actions we will hand the model state dictionary to the Request.CreateErrorResponse extension and wrap it in a HttpResponseException. Adding the model state dictionary results in a list of the model errors sent in the response body.

  2. For errors that occur in higher layers, server errors, we let the exception bubble to the Web API app, here we have a global exception filter which looks at the exception, logs it with elmah and trys to make sense of it setting the correct http status code and a relevant friendly error message as the body again in a HttpResponseException. For exceptions that we aren't expecting the client will receive the default 500 internal server error, but a generic message due to security reasons.

Update 3

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

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

Похожие вопросы