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

时间:2012-05-24 07:00:14

标签: c# rest 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);
}

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

11 个答案:

答案 0 :(得分:262)

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

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

我的应用程序中的一个示例是,有时客户端会询问数据,但没有任何数据可用,所以我抛出一个自定义noDataAvailableException并让它冒泡到web 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

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

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

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

  3. 更新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);
        }
    }
    

答案 1 :(得分:163)

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

链接:

答案 2 :(得分:72)

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

<强>验证

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

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

错误处理

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

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

答案 3 :(得分:35)

你可以抛出一个HttpResponseException

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

答案 4 :(得分:22)

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

public IHttpActionResult Save(MyEntity entity)
{
  ....

    return ResponseMessage(
        Request.CreateResponse(
            HttpStatusCode.BadRequest, 
            validationErrors));
}

答案 5 :(得分:10)

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

return BadRequest("message");

答案 6 :(得分:4)

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

答案 7 :(得分:4)

建立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,无论操作是否成功。 E.g:

    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 :(得分:3)

使用内置&#34; InternalServerError&#34;方法(在ApiController中可用):

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

答案 9 :(得分:0)

只是为了更新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 :(得分:-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)"
    ]