Рекомендуется возвращать ошибки в ASP.NET Web API

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

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

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

11 ответов


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

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

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

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

обновление:

С тех пор, как я ответил на этот вопрос, было написано несколько сообщений в блоге по теме:

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

обновление 2

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

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

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

обновление 3

недавно, после сбора веб-API 2, для отправки общих ошибок мы теперь используем IHttpActionResult интерфейс, в частности встроенные классы для в системе.Сеть.Http.Пространство имен результатов, такое как 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);
    }
}

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 ниже.)

ссылки:


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

проверка

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

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

затем вы можете использовать ActionFilter это автоматически отправляет сообщения valiation обратно клиенту.

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

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

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

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

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

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

вы можете использовать пользовательский 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.цезий конфиг.Фильтры.Add (new DRFValidationFilters ());


дом по Manish Jainответ (который предназначен для веб-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"));
        }
    }

используйте встроенный метод" InternalServerError " (доступный в ApiController):

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

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

return BadRequest("message");

просто обновить текущее состояние 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
}

для тех ошибок, где 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)"
    ]