46

I have an ASP.NET Core 1.0 Web API application and trying to figure out how to pass the exception message to the client if a function that my controller is calling errors out.

I have tried so many things, but nothing implements IActionResult.

I don't understand why this isn't a common thing that people need. If there truthfully is no solution can someone tell me why?

I do see some documentation out there using HttpResponseException(HttpResponseMessage), but in order to use this, I have to install the compat shim. Is there a new way of doing these things in Core 1.0?

Here is something I have been trying with the shim but it isn't working:

// GET: api/customers/{id}
[HttpGet("{id}", Name = "GetCustomer")]
public IActionResult GetById(int id)
{
    Customer c = _customersService.GetCustomerById(id);
    if (c == null)
    {
        var response = new HttpResponseMessage(HttpStatusCode.NotFound)
        {
            Content = new StringContent("Customer doesn't exist", System.Text.Encoding.UTF8, "text/plain"),
            StatusCode = HttpStatusCode.NotFound

        };

        throw new HttpResponseException(response);

        //return NotFound();
    }
    return new ObjectResult(c);
}

When the HttpResponseException is thrown, I look on the client and can't find the message I am sending anything in the content.

Set
  • 47,577
  • 22
  • 132
  • 150
Blake Rivell
  • 13,105
  • 31
  • 115
  • 231
  • In ASP.Net we usually log the error, instead of showing it to the user, we show the user a JavaScript alert that an error occurred. – Ashraf Sada Jun 24 '16 at 13:25
  • @AshrafAbusada is that really the case? How are you able to give the user a detailed alert in JavaScript. Lets say I have a really complex in the business logic of my Web API that can throw a couple of different exceptions such as: "Customers not setup properly" and "Products invalid" when this exception is thrown there is really no way of passing it to the client in the header or content of the response? – Blake Rivell Jun 24 '16 at 13:34
  • @BlakeRivell we wrap an error into own custom error code, or into custom error DTO object if more detailed msg is needed, and send this in response to client (also changing the response status code to show HTTP code error like 500). Then Client analyse, that HTTP status code is not 200 OK and shows the predefined Alert msg accordingly to custom error code (+ data from custom error DTO) – Set Jun 24 '16 at 13:42
  • @Set Alright, so you are saying to create an ErrorDTO and send that back in place of the CustomerDTO? Can you show me an example? Wont result.IsSuccess still be true? – Blake Rivell Jun 24 '16 at 13:46
  • There is a pattern over HTTP called RESTful. It means that in the event of error, first of all you must decide WHO can deal with this. In the case of Invalid Products or Customers, only high up people can (judgement) so throwing a 500 would be a good start. If it is, this data is malformed, then you throw a 400. The client will then handle these codes appropriately (and basing the return result on the http code) – Callum Linington Jun 24 '16 at 13:49
  • @CallumLinington So I will never be able to tell the client exact messages like: "Invalid Products" or "Invalid Customers" specifically unless I use custom error codes or something and read them? Doesn't seem too friendly. Does the solution that Set just proposed seem better for me? – Blake Rivell Jun 24 '16 at 13:55
  • 1
    You can tell your customers what ever you like, the status code just dictates the way your client will interpret the response. Plus, they're not custom error codes, they called HTTP Status Codes. – Callum Linington Jun 24 '16 at 14:22
  • @CallumLinington I see so your saying the client developer should know which status code means what. Whether it is through api documentation or if I am the client developer and don't need documentation. Look at the code and then write the correct message. Is this the correct assumption? – Blake Rivell Jun 24 '16 at 14:29
  • 1
    The HTTP Status Codes are a standard, and have well written documentation surrounding them. Any client side developer worth their weight should know about most of them. However, the response that you send down to the client will have an object describing the result - thus it should be trivial to know what to do with it. – Callum Linington Jun 24 '16 at 14:36
  • @CallumLinington I understand the HTTP Status Codes part, I just need to know how the client developer knows whether Customers were invalid or Products were invalid. Are you saying the object I return should be some sort of Json Error object that has a message in it? – Blake Rivell Jun 24 '16 at 14:37
  • But what can the Client Developer do with this information? Can the Client application handle it? if so you send it down as the response body. – Callum Linington Jun 24 '16 at 15:57

7 Answers7

67

Here is an simple error DTO class

public class ErrorDto
{
    public int Code {get;set;}
    public string Message { get; set; }

    // other fields

    public override string ToString()
    {
        return JsonConvert.SerializeObject(this);
    }
}

And then using the ExceptionHandler middleware:

            app.UseExceptionHandler(errorApp =>
            {
                errorApp.Run(async context =>
                {
                    context.Response.StatusCode = 500; // or another Status accordingly to Exception Type
                    context.Response.ContentType = "application/json";

                    var error = context.Features.Get<IExceptionHandlerFeature>();
                    if (error != null)
                    {
                        var ex = error.Error;

                        await context.Response.WriteAsync(new ErrorDto()
                        {
                            Code = <your custom code based on Exception Type>,
                            Message = ex.Message // or your custom message
                            // other custom data
                        }.ToString(), Encoding.UTF8);
                    }
                });
            });
Set
  • 47,577
  • 22
  • 132
  • 150
  • this is a great solution and I believe it follows the new asp.net core documentation: https://docs.asp.net/en/latest/fundamentals/error-handling.html# I will give it a go. – Blake Rivell Jun 24 '16 at 14:00
  • would I put it before or after app.UseMVC? – Blake Rivell Jun 24 '16 at 14:04
  • 3
    place it before. app.UseMVC() in most cases should be the last one. – Set Jun 24 '16 at 14:05
  • And I would just simply throw an exception somewhere in my controller to test? – Blake Rivell Jun 24 '16 at 14:10
  • yep, that's enough – Set Jun 24 '16 at 14:15
  • I am using System.Net.Http in C# to make the web api call from a repository in another application. I got as far as receiving the error and deserializing it into an Error object instead of a Customer object, but my function returns an object of type Customer so is there anyway it can return the error all the way up to my code behind of my aspx page? – Blake Rivell Jun 24 '16 at 14:17
  • not sure that understand you correctly, but if you mean that your Controller returns data of type Customer if all is OK, and ask if you can use any other Type in response - the answer is yes. HTTP is hyper-text transfer protocol - everything is a text in the end. In Client you should/may look on response Status Code to choose how to parse response. – Set Jun 24 '16 at 14:28
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/115533/discussion-between-blake-rivell-and-set). – Blake Rivell Jun 24 '16 at 14:29
  • is it possible to use app.UseExceptionHandler with default error page? I want to capture error details, but keep existing behaviour and pages. – Semen Shekhovtsov Dec 08 '16 at 11:51
  • Wouldn't the body get blocked by IIS? In the past, IIS would override any content you try to return from a "failed" http call. See https://weblog.west-wind.com/posts/2009/Apr/29/IIS-7-Error-Pages-taking-over-500-Errors – Mark Mar 10 '17 at 15:19
10

Yes it is possible to change the status code to whatever you need:

In your CustomExceptionFilterAttribute.cs file modify the code as follows:

public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        var exception = context.Exception;
        context.Result = new ContentResult
        {
            Content = $"Error: {exception.Message}",
            ContentType = "text/plain",
            // change to whatever status code you want to send out
            StatusCode = (int?)HttpStatusCode.BadRequest 
        };
    }
}

That's pretty much it.

If you have custom exceptions, then you can also check for them when grabbing the thrown exception from the context. Following on from that you can then send out different HTTP Status Codes depdending on what has happened in your code.

Hope that helps.

Christoph
  • 2,211
  • 1
  • 16
  • 28
shabbirh
  • 675
  • 1
  • 5
  • 13
  • 1
    Add ";" at the and of "ContentResult" and replace "test/plain" with "text/plain" or better with "System.Net.Mime.MediaTypeNames.Text.Plain". – Skorunka František Mar 10 '17 at 09:37
  • I just corrected the types for shabbirh. @Skorunka František, in which Nuget package do you find System.Net.Mime.MediaTypeNames.Text.Plain? I'm using ASP.Net Core 1.1.1. – Christoph Apr 09 '17 at 04:08
  • @Christoph: I believe, it is part of system.dll https://msdn.microsoft.com/en-us/library/system.net.mime.mediatypenames.text.plain(v=vs.110).aspx – Skorunka František Apr 10 '17 at 08:10
6

You can create a custom Exception Filter like below

public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        var exception = context.Exception;
        context.Result = new JsonResult(exception.Message);
    }
}

Then apply the above attribute to your controller.

[Route("api/[controller]")]
[CustomExceptionFilter]
public class ValuesController : Controller
{
     // GET: api/values
    [HttpGet]
    public IEnumerable<string> Get()
    {
        throw new Exception("Suckers");
        return new string[] { "value1", "value2" };
    }
}
cvraman
  • 1,687
  • 1
  • 12
  • 18
  • I would also add exception handling based on type as well, so you can then determine the status code to return back as well – Callum Linington Jun 24 '16 at 13:50
  • 1
    @CallumLinington This looks like a good solution, but the status code still returns back as OK. Is there anyway to change it? Additionally, where do I look in the response for the message "Suckers"? I can't find it anywhere. – Blake Rivell Jun 24 '16 at 13:52
  • You can also add the filter to all controllers in ConfigureServices(IServiceCollection services) in Startup.cs by doing services.AddMvc(config => { services.Filters.Add(typeof(CustomExceptionFilterAttribute)); }); – tifkin Dec 31 '16 at 21:04
  • @Blake Rivell: Take a look at shabbirh's solution. That one shows how to modify the HTTP status code. – Christoph Apr 09 '17 at 04:09
5

Rather than raising and catching an exception, how about you simplify your action to:

// GET: api/customers/{id}
[HttpGet("{id}", Name = "GetCustomer")]
public IActionResult GetById(int id)
{
    var customer = _customersService.GetCustomerById(id);

    if (customer == null)
    {
        return NotFound("Customer doesn't exist");        
    }

    return Ok(customer);
}

I wrote a blog post with some more options such as returning a JSON object instead of text.

Paul Hiles
  • 9,558
  • 7
  • 51
  • 76
  • 1
    this is a custom solution, as it does not work for cases when code like `_customersService.GetCustomerById(id);` could throws an exception internally. – Set May 27 '17 at 08:20
  • true. it is just a simper, working version of the code in the original post. – Paul Hiles May 27 '17 at 08:22
  • The OP states "that my controller is calling errors out". There is no mention of the service throwing exceptions. Just wanted to show that in the example code, exceptions are not necessary for a simple 404. – Paul Hiles May 27 '17 at 08:24
  • This always assumes that the db call is done in Controller, how do you return NotFound() from a service inside (not from controller), different project, class ? – Idothisallday Oct 04 '18 at 16:52
  • 1
    That would likely be a server side error. You should catch it and return a 500 instead. A class can return a custom error as well, since those should be handled internally and the controller can process that result accordingly. – Jason Conville Dec 11 '18 at 16:37
1

Maybe that is helpful. You can return just object and sent for example a BadRequest (HTTP CODE: 400) with your custom object as actual parameter (I just used an interpolated string here) but you can put in anything.

In your client side you can catch that error situation for example with an AJAX error handler.

// GET: api/TruckFahrerGeoData
[HttpGet]
public object GetTruckFahrerGeoData()
{

    var truckFahrerGeoDataItems = new List<TruckFahrerGeoDataViewModel>();

    var geodataItems = _context.TruckFahrerGeoData;

    foreach (var truckFahrerGeoData in geodataItems)
    {
        GeoTelemetryData geoTelemetryData = JsonConvert.DeserializeObject<GeoTelemetryData>(truckFahrerGeoData.TelemetryData);

        if (geoTelemetryData == null)
        {
            return BadRequest($"geoTelemetryData null for id: {truckFahrerGeoData.Id}");
        }
        TruckFahrerGeoDataViewModel truckFahrerGeoDataViewModel = new TruckFahrerGeoDataViewModel
        {
            Speed = geoTelemetryData.Speed,
            Accuracy = geoTelemetryData.Accuracy,
            TruckAppId = geoTelemetryData.Activity.TruckAppId,
            TruckAuftragStatusId = geoTelemetryData.Activity.TruckAuftragStatusId,
            ClId = geoTelemetryData.Activity.ClId,
            TruckAuftragLaufStatusId = geoTelemetryData.Activity.TruckAuftragLaufStatusId,
            TaskId = geoTelemetryData.Activity.TaskId,
            TruckAuftragWorkflowStatusId = geoTelemetryData.Activity.TruckAuftragWorkflowStatusId
        };

        truckFahrerGeoDataItems.Add(truckFahrerGeoDataViewModel);
    }


    return truckFahrerGeoDataItems;
}

Or an even more cleaner way with IActionResult like that way:

// GET: api/TruckFahrerGeoData
[HttpGet]
public IActionResult GetTruckFahrerGeoData()
{

    var truckFahrerGeoDataItems = new List<TruckFahrerGeoDataViewModel>();

    var geodataItems = _context.TruckFahrerGeoData;

    foreach (var truckFahrerGeoData in geodataItems)
    {
        GeoTelemetryData geoTelemetryData = JsonConvert.DeserializeObject<GeoTelemetryData>(truckFahrerGeoData.TelemetryData);

        if (geoTelemetryData == null)
        {
            return BadRequest($"geoTelemetryData null for id: {truckFahrerGeoData.Id}");
        }
        TruckFahrerGeoDataViewModel truckFahrerGeoDataViewModel = new TruckFahrerGeoDataViewModel
        {
            Speed = geoTelemetryData.Speed,
            Accuracy = geoTelemetryData.Accuracy,
            TruckAppId = geoTelemetryData.Activity.TruckAppId,
            TruckAuftragStatusId = geoTelemetryData.Activity.TruckAuftragStatusId,
            ClId = geoTelemetryData.Activity.ClId,
            TruckAuftragLaufStatusId = geoTelemetryData.Activity.TruckAuftragLaufStatusId,
            TaskId = geoTelemetryData.Activity.TaskId,
            TruckAuftragWorkflowStatusId = geoTelemetryData.Activity.TruckAuftragWorkflowStatusId
        };

        truckFahrerGeoDataItems.Add(truckFahrerGeoDataViewModel);
    }


    return Ok(truckFahrerGeoDataItems);
}
Erich Brunner
  • 612
  • 5
  • 19
1

Late to the party but refining the answer .

Define your error response class with minimum below attributes

using Microsoft.AspNetCore.Http;

    public class ErrorResponse
        {
            private readonly RequestDelegate next;
            public ErrorResponse(RequestDelegate next)
            {
                this.next = next;
            }
    
            public async Task Invoke(HttpContext context )
            {
                try
                {
                    await next(context);
                }
                catch (Exception ex)
                {
                    await HandleExceptionAsync(context, ex);
                }
            }
    
            private static Task HandleExceptionAsync(HttpContext context, Exception ex)
            {
                var code = HttpStatusCode.InternalServerError;         
                string result = string.Empty;
                object data = new object();
                if (ex is ForbiddenException)
                {
                    code = HttpStatusCode.Forbidden;
                    result = JsonConvert.SerializeObject(new Response<object>(Status.Forbidden(ex.Message), data));
                }
                else if(ex is BadRequestException){
                    code = HttpStatusCode.BadRequest;
                    result = JsonConvert.SerializeObject(new Response<object>(Status.BadRequest(ex.Message), data));
                }
                else if (ex is NotFoundException)
                {
                    code = HttpStatusCode.NotFound;
                    result = JsonConvert.SerializeObject(new Response<object>(Status.NotFound(ex.Message), data));
                }
                else if (ex is UnauthorizedException)
                {
                    code = HttpStatusCode.Unauthorized;
                    result = JsonConvert.SerializeObject(new Response<object>(Status.Unauthorized(ex.Message), data));
                }
                else
                {
                    result = JsonConvert.SerializeObject(new Response<object>(Status.InternalServerError(ex.Message), data));
                }
    
    
                context.Response.ContentType = "application/json";
                context.Response.StatusCode = (int)code;
                return context.Response.WriteAsync(result);
            }
        }

Next use this class as middleware in startup.cs class

app.UseHttpsRedirection();
app.UseMiddleware(typeof(ErrorResponse)); 

Now each request and response will go through this class,if an error occurs then error code will be set to true with error code. A sample response like below

data: {}
status: {
code: 404
error: true
message: "No employee data found"
type: "Not Found"
}
Hameed Syed
  • 3,939
  • 2
  • 21
  • 31
0

I had the same problem and after some research, I found out I could use HttpClient to call my API and read the response easily. HttpClient does not throw any error when the HTTP response contains an error code, but it sets the IsSuccessStatusCode property to false.

This is my function using the HttpClient. I call this from my controller.

  public static async Task<HttpResponseMessage> HttpClientPost(string header, string postdata, string url)
        {
            string uri = apiUrl + url;
            using (var client = new HttpClient())
            {
                //client.BaseAddress = new Uri(uri);
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", header);
                HttpResponseMessage response = await client.PostAsync(uri, new StringContent(postdata));

                return response;
            }
        }

This is my controller code, where I call the function and read the response and determine whether I have an error or not and respond accordingly. Note that I am checking the IsSuccessStatusCode.

                HttpResponseMessage response;
                string url = $"Setup/AddDonor";
                var postdata = JsonConvert.SerializeObject(donor);

                response = await ApiHandler.HttpClientPost(HttpContext.Session.GetString(tokenName), postdata, url);
                //var headers = response.Headers.Concat(response.Content.Headers);
                var responseBody = await response.Content.ReadAsStringAsync();

                if (response.IsSuccessStatusCode)
                {
                    tnxresult = JsonConvert.DeserializeObject<TnxResult>(AppFunctions.CleanResponse(responseBody));

                    return Json(new
                    {
                        ok = true,
                        message = tnxresult.Message,
                        statusCode = tnxresult.StatusCode
                    });
                }
                else
                {
                  ApiError rs = JsonConvert.DeserializeObject<ApiError>(AppFunctions.CleanResponse(responseBody));

                    return Json(new
                    {
                        ok = false,
                        message = rs.Message,
                        statusCode = rs.StatusCode
                    });

                }

My API returns error messages in JSON. If the call is successful, I am packing the response in JSON too.

The crucial line of code is this one...

var responseBody = await response.Content.ReadAsStringAsync();

It serializes the HTTP content to a string as an asynchronous operation.

After that I can convert my JSON string to an object and access the error/success message and the Status Code too.

Onsongo Moseti
  • 439
  • 5
  • 10
  • 2
    It is recommended NOT to wrap the HttpClient in a using statement. Better to make your HttpClient Static. https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/ – Onsongo Moseti Jun 19 '19 at 01:35