Best practice to return errors in ASP.NET Web API

2019-01-03 07:35发布

I have concerns on the way that we returns errors to client.

Do we return error immediately by throwing HttpResponseException when we get an error:

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

Or we accumulate all errors then send back to client:

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

This is just a sample code, it does not matter either validation errors or server error, I just would like to know the best practice, the pros and cons of each approach.

11条回答
【Aperson】
2楼-- · 2019-01-03 07:42

For Web API 2 my methods consistently return IHttpActionResult so I use...

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

    return ResponseMessage(
        Request.CreateResponse(
            HttpStatusCode.BadRequest, 
            validationErrors));
}
查看更多
贼婆χ
3楼-- · 2019-01-03 07:43

It looks like you're having more trouble with Validation than errors/exceptions so I'll say a bit about both.

Validation

Controller actions should generally take Input Models where the validation is declared directly on the model.

public class Customer
{ 
    [Require]
    public string Name { get; set; }
}

Then you can use an ActionFilter that automatically sends valiation messages back to the client.

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

For more information about this check out http://ben.onfabrik.com/posts/automatic-modelstate-validation-in-aspnet-mvc

Error handling

It's best to return a message back to the client that represents the exception that happened (with relevant status code).

Out of the box you have to use Request.CreateErrorResponse(HttpStatusCode, message) if you want to specify a message. However, this ties the code to the Request object, which you shouldn't need to do.

I usually create my own type of "safe" exception that I expect the client would know how to handle and wrap all others with a generic 500 error.

Using an action filter to handle the exceptions would look like this:

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

Then you can register it globally.

GlobalConfiguration.Configuration.Filters.Add(new ApiExceptionFilterAttribute());

This is my custom exception type.

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

An example exception that my API can throw.

public class NotAuthenticatedException : ApiException
{
    public NotAuthenticatedException()
        : base(HttpStatusCode.Forbidden)
    {
    }
}
查看更多
祖国的老花朵
4楼-- · 2019-01-03 07:44

For me I usually send back a HttpResponseException and set the status code accordingly depending on the exception thrown and if the exception is fatal or not will determine whether I send back the HttpResponseException immediately.

At the end of the day its an API sending back responses and not views, so I think its fine to send back a message with the exception and status code to the consumer. I currently haven't needed to accumulate errors and send them back as most exceptions are usually due to incorrect parameters or calls etc.

An example in my app is that sometimes the client will ask for data, but there isnt any data available so i throw a custom noDataAvailableException and let it bubble to the web api app, where then in my custom filter which captures it sending back a relevant message along with the correct status code.

I am not 100% sure on whats the best practice for this, but this is working for me currently so thats what im doing.

Update:

Since i answered this question a few blog posts have been written on the topic:

http://weblogs.asp.net/fredriknormen/archive/2012/06/11/asp-net-web-api-exception-handling.aspx

(this one has some new features in the nightly builds) http://blogs.msdn.com/b/youssefm/archive/2012/06/28/error-handling-in-asp-net-webapi.aspx

Update 2

Update to our error handling process, we have two cases:

  1. For general errors like not found, or invalid parameters being passed to an action we return a HttpResponseException to stop processing immediately. Additionally for model errors in our actions we will hand the model state dictionary to the Request.CreateErrorResponse extension and wrap it in a HttpResponseException. Adding the model state dictionary results in a list of the model errors sent in the response body.

  2. For errors that occur in higher layers, server errors, we let the exception bubble to the Web API app, here we have a global exception filter which looks at the exception, logs it with elmah and trys to make sense of it setting the correct http status code and a relevant friendly error message as the body again in a HttpResponseException. For exceptions that we aren't expecting the client will receive the default 500 internal server error, but a generic message due to security reasons.

Update 3

Recently, after picking up Web API 2, for sending back general errors we now use the IHttpActionResult interface, specifically the built in classes for in the System.Web.Http.Results namespace such as NotFound, BadRequest when they fit, if they dont we extend them, for example a notfound result with a response message:

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);
    }
}
查看更多
仙女界的扛把子
5楼-- · 2019-01-03 07:45

You can throw a HttpResponseException

HttpResponseMessage response = 
    this.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "your message");
throw new HttpResponseException(response);
查看更多
兄弟一词,经得起流年.
6楼-- · 2019-01-03 07:47

Use the built in "InternalServerError" method (available in ApiController):

return InternalServerError();
//or...
return InternalServerError(new YourException("your message"));
查看更多
时光不老,我们不散
7楼-- · 2019-01-03 07:59

If you are using ASP.NET Web API 2, the easiest way is to use the ApiController Short-Method. This will result in a BadRequestResult.

return BadRequest("message");
查看更多
登录 后发表回答