【问题标题】:Best practice to return errors in ASP.NET Web API在 ASP.NET Web API 中返回错误的最佳实践
【发布时间】:2012-05-30 18:01:32
【问题描述】:

我对我们向客户返回错误的方式感到担忧。

当我们得到错误时,我们是否会立即通过抛出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# rest asp.net-web-api


【解决方案1】:

试试这个

[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,只是论坛上的“试试这个”回复。也许你可以分享源链接。
【解决方案2】:

对于 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的引用
【解决方案3】:

您可以在 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());

【讨论】:

    【解决方案4】:

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

    验证

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

    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 类的示例?
    【解决方案5】:

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

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

    1. 对于一般错误,例如未找到,或传递给操作的参数无效,我们返回 HttpResponseException 以立即停止处理。此外,对于我们操作中的模型错误,我们会将模型状态字典交给Request.CreateErrorResponse 扩展并将其包装在HttpResponseException 中。添加模型状态字典会生成响应正文中发送的模型错误列表。

    2. 对于发生在更高层的错误,服务器错误,我们让异常冒泡到 Web API 应用程序,这里我们有一个全局异常过滤器,它查看异常,使用 ELMAH 记录它并尝试使其有意义它在HttpResponseException 中再次将正确的HTTP 状态代码和相关的友好错误消息设置为正文。对于我们不期望客户端会收到默认的 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 即便如此,它确实有两个组件,验证和异常,所以最好同时涵盖两者。
    【解决方案6】:

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

    return BadRequest("message");
    

    【讨论】:

    • 仅针对模型验证错误,我使用了接受 ModelState 对象的 BadRequest() 的重载:return BadRequest(ModelState);
    【解决方案7】:

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

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

    【讨论】:

      【解决方案8】:

      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) 服务层无论操作成功与否,都会返回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"));
              }
          }
      

      【讨论】:

        【解决方案9】:

        对于那些 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 和测试期间的问题。
        【解决方案10】:

        只是为了更新 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 改变了这一切
        【解决方案11】:

        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 链接。)

        链接:

        【讨论】:

        • 我会更进一步,从 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 特定序列化信息,例如“schemas.microsoft.com/2003/10/Serialization/">My error here”。对于适合 400 状态的情况我发现 ApiController.BadRequest(string message) 返回更好的“My error here”字符串。但我找不到返回 500 状态的等价物用一个简单的消息。
        【解决方案12】:

        你可以抛出一个 HttpResponseException

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

        【讨论】:

        • 这些类/方法似乎在 .net 5 中不可用。也许 BadHttpRequestException 取代了这些。
        猜你喜欢
        • 2016-10-27
        • 2015-07-05
        • 1970-01-01
        • 2018-03-26
        • 1970-01-01
        • 1970-01-01
        • 2013-03-14
        • 2019-03-16
        相关资源
        最近更新 更多