首页 文章

在ASP.NET Web API中返回错误的最佳实践

提问于
浏览
306

我对我们向客户返回错误的方式表示担忧 .

我们在收到错误时抛出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 回答

  • 1

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

    return BadRequest("message");
    
  • -2

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

    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 Error 500 . 选择一个代表数据状态的相应HTTP状态代码 . (参见下面的apigee链接 . )

    链接:

  • 63

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

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

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

    在一天结束时,它发送回应用而不是视图的API,所以我认为可以将带有异常和状态代码的消息发送给消费者 . 我目前不需要累积错误并将其发回,因为大多数异常通常是由于不正确的参数或调用等 .

    我的应用程序中的一个示例是,有时客户端会询问数据,但没有任何数据可用,所以我抛出一个自定义的noDataAvailableException并让它冒泡到web api应用程序,然后在我的自定义过滤器中捕获它发送回相关的消息以及正确的状态代码 .

    我不是百分之百确定这是什么最好的做法,但这对我来说是有用的,所以我正在做什么 .

    Update

    自从我回答了这个问题后,就这个话题撰写了几篇博文:

    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

    Update 2

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

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

    • 对于更高层中发生的错误,服务器错误,我们让异常气泡到Web API应用程序,这里我们有一个全局异常过滤器查看异常,用elmah和trys记录它以理解它设置正确在HttpResponseException中再次将http状态代码和相关的友好错误消息作为正文 . 对于我们不期望的异常,客户端将收到默认的500内部服务器错误,但由于安全原因会收到通用消息 .

    Update 3

    最近,在拿起Web API 2之后,为了发回一般错误,我们现在使用IHttpActionResult接口,特别是System.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 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
    }
    
  • 30

    看起来你的验证比错误/异常更麻烦,所以我会说两点 .

    Validation

    控制器操作通常应采用输入模型,其中验证直接在模型上声明 .

    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

    Error handling

    最好将消息返回给客户端,代表发生的异常(带有相关的状态代码) .

    开箱即用,如果要指定消息,则必须使用 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)
        {
        }
    }
    
  • 16

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

  • 245

    Build Manish Jain 的答案(适用于简化事物的Web API 2):

    1)使用 validation structures 响应尽可能多的验证错误 . 这些结构也可用于响应来自表单的请求 .

    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) Service layer 将返回 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 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"));
            }
        }
    
  • 2

    你可以抛出一个HttpResponseException

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

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

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

相关问题