Лучшая практика для возврата ошибок в 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);
}
Это всего лишь пример кода, это не имеет значения ни ошибок проверки, ни ошибок сервера, я просто хотел бы узнать лучшие практики, плюсы и минусы каждого подхода.
Ответы
Ответ 1
Для меня я обычно отправляю обратно HttpResponseException
и соответствующим образом устанавливаю код состояния в зависимости от генерируемого исключения и если исключение является фатальным или не будет определять, немедленно ли я отправлю HttpResponseException
.
В конце дня его API отсылает ответы, а не просмотры, поэтому я думаю, что это прекрасно, чтобы отправить сообщение с кодом исключения и статуса для потребителя. В настоящее время мне не нужно накапливать ошибки и отправлять их обратно, поскольку большинство исключений обычно вызвано неправильными параметрами или вызовами и т.д.
Примером моего приложения является то, что иногда клиент запрашивает данные, но нет доступных данных, поэтому я бросаю пользовательское исключение noDataAvailableException и позволяю ему пузыриться в веб-приложении 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
Обновите наш процесс обработки ошибок, у нас есть два случая:
-
Для общих ошибок, подобных не найденным, или недопустимых параметров, передаваемых в действие, мы возвращаем исключение HttpResponseException, чтобы немедленно прекратить обработку. Кроме того, для ошибок модели в наших действиях мы передадим словарь состояния модели в расширение Request.CreateErrorResponse
и перенесем его в исключение HttpResponseException. Добавление слова состояния модели приводит к списку ошибок модели, отправленных в тело ответа.
-
Для ошибок, возникающих на более высоких уровнях, ошибок сервера, мы позволяем пузырьку исключения в приложении веб-API, здесь мы имеем глобальный фильтр исключений, который рассматривает исключение, регистрирует его с помощью elmah и пытается понять из него установка правильного кода статуса http и соответствующее дружественное сообщение об ошибке как тело снова в исключении HttpResponseException. Для исключений, которые мы не ожидаем, клиент получит внутреннюю ошибку сервера 500 по умолчанию, но общее сообщение по причинам безопасности.
Обновление 3
Недавно, получив Web API 2, для отправки общих ошибок мы теперь используем интерфейс IHttpActionResult, в частности встроенные классы для системы. 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);
}
}
Ответ 2
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 ниже.)
Ссылки:
Ответ 3
Похоже, у вас больше проблем с проверкой, чем с ошибками/исключениями, поэтому я немного расскажу о них.
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
Обработка ошибок
Лучше всего вернуть сообщение клиенту, представляющему исключение, которое произошло (с правильным кодом состояния).
Из коробки вы должны использовать 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)
{
}
}
Ответ 4
Вы можете создать исключение HttpResponseException
HttpResponseMessage response =
this.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "your message");
throw new HttpResponseException(response);
Ответ 5
Для Web API 2 мои методы последовательно возвращают IHttpActionResult, поэтому я использую...
public IHttpActionResult Save(MyEntity entity)
{
....
return ResponseMessage(
Request.CreateResponse(
HttpStatusCode.BadRequest,
validationErrors));
}
Ответ 6
вы можете использовать пользовательский 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());
Ответ 7
Настройте ответ 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) Сервисный уровень вернет ValidationResult
s, независимо от успешной операции или нет. Например:
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"));
}
}
Ответ 8
Использовать встроенный метод "InternalServerError" (доступный в ApiController):
return InternalServerError();
//or...
return InternalServerError(new YourException("your message"));
Ответ 9
Просто обновить текущее состояние 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
}
Ответ 10
Для тех ошибок, в которых 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)"
]