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

这只是一个示例代码,无论是验证错误还是服务器错误都不重要,我只是想知道最佳实践,每种方法的优点和缺点。

672311 次浏览

对我来说,我通常返回HttpResponseException,并根据抛出的异常设置相应的状态代码,如果异常是致命的,将决定是否立即返回HttpResponseException

在一天结束的时候,它是一个返回响应而不是视图的API,所以我认为向使用者发送带有异常和状态代码的消息是很好的。我目前还不需要积累错误并将它们发送回来,因为大多数异常通常是由于不正确的参数或调用等。

在我的应用程序中的一个例子是,有时客户端会要求数据,但没有任何可用的数据,所以我抛出一个自定义NoDataAvailableException,并让它气泡到Web API应用程序,然后在我的自定义过滤器捕捉它发送回一个相关的消息以及正确的状态代码。

我不是100%确定这方面的最佳实践是什么,但这对我来说目前是有效的,所以这就是我正在做的。

更新:

自从我回答了这个问题,就有一些关于这个话题的博客文章:

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

(这个在夜间构建中有一些新特性) https://learn.microsoft.com/archive/blogs/youssefm/error-handling-in-asp-net-webapi < / p >

更新2

更新我们的错误处理过程,我们有两种情况:

  1. 对于一般错误,如未找到,或无效的参数被传递给一个动作,我们返回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);
}
}

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

建议:除非有灾难性错误(例如WCF Fault Exception),否则不要抛出HTTP Error 500。选择一个表示数据状态的适当HTTP状态代码。(请参阅下面的apigee链接。)

链接:

你可以抛出一个HttpResponseException

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

看起来你在验证方面遇到的麻烦比错误/异常更多,所以我将对两者都说一点。

验证

控制器动作通常应该采用Input Models,其中验证直接在模型上声明。

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

对于那些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)"
]

对于Web API 2,我的方法始终返回IHttpActionResult,所以我使用…

public IHttpActionResult Save(MyEntity entity)
{
....
if (...errors....)
return ResponseMessage(
Request.CreateResponse(
HttpStatusCode.BadRequest,
validationErrors));


// otherwise success
return Ok(returnData);
}

你可以在Web Api中使用自定义ActionFilter来验证模型:

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();
}
}
}
在webApiConfig.cs中注册CustomAttribute类 config.Filters。添加(新DRFValidationFilters ()); < / p >

只是更新一下ASP的当前状态。净之前。接口现在被称为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
}

基于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)不管操作是否成功,服务层将返回__abc0。例句:

    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 Short-Method。这将导致一个BadRequestResult。

return BadRequest("message");

试试这个

[HttpPost]
public async Task<ActionResult<User>> PostUser(int UserTypeId, User user)
{
if (somethingFails)
{
// Return the error message like this.
return new BadRequestObjectResult(new
{
message = "Something is not working here"
});
}


return ok();
}

其中一些答案似乎是过去的遗迹。我发现下面的解决方案既简单又有效。这是在。net 6中的Web API 派生自ControllerBase

而不是抛出异常,你可以直接返回各种HTTP响应代码作为对象,以及一个准确的错误消息:

using Microsoft.AspNetCore.Mvc;


[ApiController]
public class MyWebApiController : ControllerBase
{
[HttpPost]
public IActionResult Process(Customer customer)
{
if (string.IsNullOrEmpty(customer.Name))
return BadRequest("Customer Name cannot be empty");


if (!Customers.Find(customer))
return NotFound("Customer does not have any account");


// After validating inputs, core logic goes here...


return Ok(customer.ID);  // or simply "return Ok()" if not returning data
}
}

查看可用的错误代码列表在这里

至于什么时候返回错误(OP的问题),这取决于需求。在错误发生时返回错误意味着您可以避免额外处理的开销,但随后客户机必须重复调用以获取所有错误。还要考虑服务器视点,因为当发生错误时,它可能导致不希望的程序行为继续服务器端处理。

欢迎来到2022年!现在我们在. net中有了其他的答案(因为ASP。NET Core 2.1)。看看这篇文章:在ASP中使用ProblemDetails类。NET核心Web API,作者解释了以下最佳实践:

  1. 如何实现标准Ietf RFC 7807,它定义了一个“问题细节”;作为一种在HTTP响应中携带机器可读的错误细节的方式,以避免为HTTP api定义新的错误响应格式。
  2. 模型验证如何使用ProblemDetails类来填充验证错误列表——这是对一般规则问题的直接回答,即在出现第一个错误后是否中断处理。

作为一个挑逗,如果我们使用ProductDetails和多个错误,JSON输出是这样的:

enter image description here