ChatGPT解决这个技术问题 Extra ChatGPT

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

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

请参阅 stackoverflow.com/a/22163675/200442 您应该使用 ModelState
请注意,此处的答案仅涵盖控制器本身引发的异常。如果您的 API 返回一个尚未执行的 IQueryable,则异常不在控制器中并且未被捕获...
非常好的问题,但不知何故我没有得到 HttpResponseException 类的任何构造函数重载,它采用您的帖子中提到的两个参数 - HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest)HttpResponseException(string, HttpStatusCode)

C
Callum Watkins

对我来说,我通常发回 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

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

对于一般错误,例如未找到,或传递给操作的参数无效,我们返回 HttpResponseException 以立即停止处理。此外,对于我们操作中的模型错误,我们会将模型状态字典交给 Request.CreateErrorResponse 扩展并将其包装在 HttpResponseException 中。添加模型状态字典会生成响应正文中发送的模型错误列表。对于发生在更高层的错误,服务器错误,我们让异常冒泡到 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?
正如我所说,这实际上取决于例外情况。致命异常,例如用户将 Web Api 无效参数传递给端点,然后我将创建一个 HttpResponseException 并将其直接返回给消费应用程序。
问题中的例外更多地是关于验证,请参阅 stackoverflow.com/a/22163675/200442
@DanielLittle 重读他的问题。我引用:“这只是一个示例代码,验证错误或服务器错误都没有关系”
@gdp 即使如此,它确实有两个组件,验证和异常,所以最好同时涵盖两者。
B
BrianS

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 故障异常),否则不要抛出 HTTP 错误 500。选择代表数据状态的适当 HTTP 状态代码。 (请参阅下面的 apigee 链接。)

链接:

ASP.NET Web API (asp.net) 中的异常处理和

RESTful API 设计:错误怎么办? (apigee.com)


我会更进一步,从 DAL/Repo 中抛出 ResourceNotFoundException,我在 Web Api 2.2 ExceptionHandler 中检查类型 ResourceNotFoundException,然后返回“未找到 ID 为 xxx 的产品”。这样,它通常锚定在架构中,而不是每个动作。
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); 有什么具体用途吗?CreateResponseCreateErrorResponse 有什么区别?
根据w3.org/Protocols/rfc2616/rfc2616-sec10.html,客户端错误是 400 级代码,服务器错误是 500 级代码。因此,在许多情况下,500 错误代码可能非常适合 Web API,而不仅仅是“灾难性”错误。
您需要 using System.Net.Http; 才能显示 CreateResponse() 扩展方法。
我不喜欢使用 Request.CreateResponse() 是它返回不必要的 Microsoft 特定序列化信息,例如“<string xmlns="schemas.microsoft.com/2003/10/Serialization/">My error here</string>"。对于 400 状态合适的情况,我发现 ApiController.BadRequest(string message) 返回了一个更好的“<Error><Message>My error here</Message></Error>”字符串。但我找不到它与返回 500 状态和简单消息的等价物.
C
Christian Casutt

看起来您在验证方面遇到的问题比错误/异常更多,所以我会说一下两者。

验证

控制器操作通常应采用直接在模型上声明验证的输入模型。

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 方法中,exception.StatusCode 无效,因为异常是 WebException。在这种情况下我能做什么?
@razp26 如果您指的是拼写错误的类似 var exception = context.Exception as WebException;,则应该是 ApiException
您能否添加一个如何使用 ApiExceptionFilterAttribute 类的示例?
t
tartakynov

你可以抛出一个 HttpResponseException

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

这些类/方法似乎在 .net 5 中不可用。也许 BadHttpRequestException 取代了这些。
F
Fabian von Ellerts

如果您使用的是 ASP.NET Web API 2,最简单的方法是使用 ApiController Short-Method。这将导致 BadRequestResult。

return BadRequest("message");

严格来说,对于模型验证错误,我使用了接受 ModelState 对象的 BadRequest() 重载:return BadRequest(ModelState);
M
Mick

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

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

    // otherwise success
    return Ok(returnData);
}

这个答案没问题,但您应该添加对 System.Net.Http 的引用
M
Metro Smurf

您可以在 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.Add(new DRFValidationFilters());


A
Alexei - check Codidact

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。例如:

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

R
Rusty

使用内置的“InternalServerError”方法(在 ApiController 中可用):

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

T
Thomas Hagström

只是为了更新 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
}

这看起来很有趣,但是这段代码具体放在项目的哪个位置?我正在 vb.net 中做我的 web api 2 项目。
它只是一个返回错误的模型,可以驻留在任何地方。您将在 Controller 中返回上述类的新实例。但老实说,我尽可能使用内置类:this.Ok()、CreatedAtRoute()、NotFound()。该方法的签名将是 IHttpActionResult。不知道他们是否用 NetCore 改变了这一切
S
Sylvain

对于那些 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 和测试期间的问题。
C
CinCout

尝试这个

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

这不适用于 WebAPI,适用于 MVC ......
这是直接从 web api repo @benjamingranados 复制的
如果您的来源是 likecs.com/ask-77378.html,仍然不是 WebApi,只是论坛上的“试试这个”响应。也许你可以分享源链接。