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


384

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

当收到错误消息时,是否通过抛出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);
}

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


1
请参阅您应该使用的stackoverflow.com/a/22163675/200442ModelState
丹尼尔·利特尔

1
请注意,此处的答案仅涵盖控制器本身引发的异常。如果您的API返回的IQueryable <Model>尚未执行,则该异常不在控制器中且未捕获...
Jess

3
一个非常好的问题,但是不知何故我没有收到HttpResponseException您的帖子中提到的两个参数的类的任何构造函数重载- HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest)HttpResponseException(string, HttpStatusCode)
RBT

Answers:


293

对我来说,我通常会发回一个,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. 对于未找到的一般错误或传递给操作的无效参数,我们将返回a 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);
    }
}

感谢您的回答geepie,这是一个很好的体验,因此您希望立即发送期望值吗?
cuongle 2012年

如我所说,这确实取决于例外情况。致命异常,例如用户将Web Api传递给端点一个无效参数,然后我将创建HttpResponseException并将其直接返回给使用的应用程序。
gdp,2012年

问题中的例外实际上是关于验证的更多信息,请参见stackoverflow.com/a/22163675/200442
丹尼尔·利特尔

1
@DanielLittle重读他的问题。我引用:“这只是示例代码,验证错误或服务器错误都没有关系”
gdp

@gdp即使如此,它确实有两个部分,即验证和异常,因此最好同时覆盖这两个部分。
丹尼尔·利特尔

184

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

链接:


4
我将更进一步,并从DAL / Repo中抛出ResourceNotFoundException,我在Web Api 2.2 ExceptionHandler中检查了ResourceNotFoundException类型,然后返回“找不到ID为xxx的产品”。这样,它通常锚定在体系结构中,而不是每个动作中。
Pascal

1
是否有任何的具体用途return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); 是什么区别CreateResponseCreateErrorResponse
Zapnologica

10
根据w3.org/Protocols/rfc2616/rfc2616-sec10.html,客户端错误是400级代码,服务器错误是500级代码。因此,在许多情况下,对于Web API,500错误代码可能非常适合,而不仅仅是“灾难性”错误。
杰西2015年

2
你需要using System.Net.Http;CreateResponse()扩展方法显示出来。
亚当·萨博

我不喜欢使用Request.CreateResponse(),因为它会返回不必要的Microsoft特定的序列化信息,例如“ <string xmlns =“ schemas.microsoft.com/2003/10/Serialization / ”> 我的错误在这里</ string >”。对于适合400状态的情况,我发现ApiController.BadRequest(string message)返回一个更好的“ <Error> <Message>我的错误在这里</ Message> </ Error>”字符串。但是我找不到一条简单的消息即可返回500状态。
vkelman '16

76

看来您在验证方面遇到的麻烦比错误/异常要多,因此我将同时介绍两者。

验证方式

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

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无效,因为exception是WebException。在这种情况下我该怎么办?
razp26

1
@ razp26如果您指的是像var exception = context.Exception as WebException;错字,那应该是ApiException
Daniel Little

2
您能否添加一个示例,说明如何使用ApiExceptionFilterAttribute类?
razp26 2015年

36

您可以抛出HttpResponseException

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

23

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

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

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

这个答案是可以的,虽然您应该添加参考System.Net.Http
Bellash

19

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

return BadRequest("message");

严格出于模型验证错误,我使用接受ModelState对象的BadRequest()重载:return BadRequest(ModelState);
timmi4sa

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 config.Filters.Add(new DRFValidationFilters())中注册CustomAttribute类。


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)服务层将返回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"));
        }
    }

3

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

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

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
}

这看起来很有趣,但是此代码在项目中的特定位置放置了什么?我正在vb.net中执行Web api 2项目。
关金

它只是返回错误的模型,可以驻留在任何地方。您将在Controller中返回上述类的新实例。但说实话,我尝试尽可能使用内置类:this.Ok(),CreatedAtRoute(),NotFound()。该方法的签名为IHttpActionResult。不知道他们是否使用NetCore改变了这一切
ThomasHagström16

-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)"
    ]

我不建议发送此详细级别的信息,除非是外部API(即公开的Internet)。您应该在过滤器中做更多的工作,并发回详细说明错误的JSON对象(如果选择的是XML格式,则返回XML),而不只是异常的ToString。
Sudhanshu Mishra'8

更正不要向外部API发送此异常。但是您可以使用它来调试内部API和测试期间的问题。
Ashish Sahu
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.