Baba
Baba

Reputation: 2229

Returning the exception from a web api method

I have the method below in my webapi. I want to be able to pass the exception to post man and see the error. I tried using "return BadRequest(ex.Message);" and I got errors because of the return type of the method.

How can I correct this such that I can return the actual error message if any?

        // GET api/Articles/News
        public IEnumerable<ArticlesDto> Get(string category)
        {
            IEnumerable<ArticlesDto> articlesByCategory = null;
            try
            {
                if (category == null)
                {

                }

               articlesByCategory = _articlesrepository.Find(category);               
            }
            catch(Exception ex)
            {
                 return BadRequest(ex.Message);
            }

            return articlesByCategory;
        }

Upvotes: 1

Views: 10514

Answers (3)

CodingYoshi
CodingYoshi

Reputation: 27039

There are a few issues with what you are doing. Let's go over them firstly and then we will go over a better approach.

Issues

  1. Do not catch an exception of type Exception and then tell the client their request is a bad request. If you have a DivideByZeroException, db not found exception, or InvalidOperationException or any other exception, you will tell the client their request is bad. This will clearly not be true.
  2. Your API is asking the client to provide you with a string for a category. So long as they provide it, even if it is "xaoudis garbage", they have done what they are supposed to do: Provide you with a string. Now it is your responsibility to do your best and provide them with a result. The result can be a list of items in that category or an error.

Returning a Response from Web API

Returning a domain object (or a DTO) is fine but if you want to have a finer level of control over the response then use HttpResponseMessage. Here is an examplef (please read the comments in code for more information):

public HttpResponseMessage Get(string category)
{
    // Step 1: First check the obvious issues
    if (string.IsNullOrWhiteSpace(category))
    {
        return Request.CreateResponse(HttpStatusCode.BadRequest);
    }

    try
    {
        // The client has sent us a category. Now we have to do our best to 
        // satisfy the request.

        // Step 2: Optional Step: First check to see if we have the category
        string cat = _categoryRepository.Get(category);
        if (string.IsNullOrWhiteSpace(cat))
        {
            var message = new HttpResponseMessage(HttpStatusCode.NotFound);
            message.Content = new StringContent($"The category with the name {category} was not found.");
            throw new HttpResponseException(message);
        }

        // Step 3: Category exists so let's return the products
        IEnumerable<ArticlesDto> articlesByCategory = _articlesrepository.Find(category);

        // Even if the list is empty, we can still return it to tell
        // the client 0 items were found
        // for the category. 
        return Request.CreateResponse(HttpStatusCode.OK, articlesByCategory);
    }
    catch (Exception ex)
    {
        // Something went wrong on our side (NOT the client's fault). So we need to:
        // 1. Log the error so we can troubleshoot it later
        // 2. Let the client know it is not their fault but our fault.
        return Request.CreateResponse(HttpStatusCode.InternalServerError);
    }
}

Web API 2

With Web API 2, you can do it like this which is much easier and cleaner. Please change the code as per your requirements.

public IHttpActionResult Get(string category)
{
    try
    {
        // code...

        return Ok(articlesByCategory);
    }
    catch (Exception ex)
    {
        // Something went wrong on our side (NOT the client's fault). So we need to:
        // 1. Log the error so we can troubleshoot it later
        // 2. Let the client know it is not their fault but our fault.
        return InternalServerError();
    }
}

Upvotes: 3

Kevin Law
Kevin Law

Reputation: 852

For you case, I think throw out the HttpResponseException with a HttpResponseMessage contains the exception message would work. Flowing code snippet has been tested my end.

    public IEnumerable<string> Get()
    {
        try
        {
            throw new InvalidOperationException("Invalid Operation");
        }
        catch(Exception ex)
        {
            var res = new HttpResponseMessage(HttpStatusCode.InternalServerError);
            res.Content = new StringContent(ex.Message);
            throw new HttpResponseException(res);
        }
    }

For more information about how to handle exception in WebAPI flow, refer to this official guide. Hope it is helpful for you.

Upvotes: 1

Christoph
Christoph

Reputation: 2317

There are probably some other ways to do this (I don't claim to be an ASP.Net Core expert) but I have solved this problem the following way. First, define a custom exception class. The purpose is that you can actually throw this without regard to any controller method return type. Also, throwing exceptions makes control flow a lot more structured.

public class CustomApiException : Exception
{
    /// <summary>
    /// Optional application-specific error code returned to the client.
    /// </summary>
    public int? ApplicationErrorCode { get; private set; } = null;

    /// <summary>
    /// HTTP status code returned to the client.
    /// </summary>
    public HttpStatusCode HttpStatusCode { get; private set; } = HttpStatusCode.BadRequest;

    public CustomApiException() : base() { }
    public CustomApiException(string message) : base(message) { }

    public CustomApiException(string message, HttpStatusCode httpStatusCode) : base(message)
    {
        HttpStatusCode = httpStatusCode;
    }

    public CustomApiException(string message, HttpStatusCode httpStatusCode, int? applicationErrorCode) : base(message)
    {
        HttpStatusCode = httpStatusCode;
        ApplicationErrorCode = applicationErrorCode;
    }

    public CustomApiException(string message, int? applicationErrorCode) : base(message)
    {
        ApplicationErrorCode = applicationErrorCode;
    }
}

Then define a custom ExceptionFilterAttribute. Please note that this copy/pasted snippet does a bit more than what you have been asking for. E.g. depending on the development vs. production it will include the entire stack trace of the exception (of any exception actually, not just CustomApiException).

// todo: turn into async filter.
public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{
    private readonly ILogger<ApiExceptionFilterAttribute> _logger;
    private readonly IHostingEnvironment _env;

    public ApiExceptionFilterAttribute(ILogger<ApiExceptionFilterAttribute> logger, IHostingEnvironment env)
    {
        _logger = logger;
        _env = env;
    }

    public override void OnException(ExceptionContext context)
    {
        _logger.LogError(new EventId(0), context.Exception, context.Exception.Message);

        dynamic errObject = new JObject();
        HttpStatusCode statusCode = HttpStatusCode.InternalServerError; // use 500 unless the API says it's a client error

        if (context.Exception.GetType() == typeof(CustomApiException))
        {
            CustomApiException customEx = (CustomApiException)context.Exception;
            if (customEx.ApplicationErrorCode != null) errObject.errorCode = customEx.ApplicationErrorCode;
            errObject.errorMessage = customEx.Message;
            statusCode = customEx.HttpStatusCode;
        }

        if (_env.IsDevelopment())
        {
            errObject.errorMessage = context.Exception.Message;
            errObject.type = context.Exception.GetType().ToString();
            errObject.stackTrace = context.Exception.StackTrace;
        }

        JsonResult result = new JsonResult(errObject);
        result.StatusCode = (int?)statusCode;
        context.Result = result;
    }
}

Finally, add the custom ExceptionFilterAttribute to the global ConfigureServices method.

public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        //...

        // Add framework services.
        services.AddMvc(options =>
            {
                options.Filters.Add(typeof(ApiExceptionFilterAttribute));
            });
    }

    // ...
}

It's a little bit of work but only one-off work, and pretty powerful once you have added it. If I remember correctly, my solution is based on this MS page Exception Handling. This may be of help if you have further questions.

Upvotes: 0

Related Questions