Error handling (Sending ex.Message to the client)
Asked Answered
F

7

47

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.

Froth answered 24/6, 2016 at 13:15 Comment(11)
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.Rishi
@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?Froth
@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)Otoscope
@Otoscope 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?Froth
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)Antarctic
@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?Froth
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.Antarctic
@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?Froth
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.Antarctic
@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?Froth
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.Antarctic
O
68

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);
                    }
                });
            });
Otoscope answered 24/6, 2016 at 13:55 Comment(10)
this is a great solution and I believe it follows the new asp.net core documentation: docs.asp.net/en/latest/fundamentals/error-handling.html# I will give it a go.Froth
would I put it before or after app.UseMVC?Froth
place it before. app.UseMVC() in most cases should be the last one.Otoscope
And I would just simply throw an exception somewhere in my controller to test?Froth
yep, that's enoughOtoscope
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?Froth
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.Otoscope
Let us continue this discussion in chat.Froth
is it possible to use app.UseExceptionHandler with default error page? I want to capture error details, but keep existing behaviour and pages.Demarcate
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 weblog.west-wind.com/posts/2009/Apr/29/…Scrutiny
H
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.

Hyrup answered 20/8, 2016 at 20:47 Comment(3)
Add ";" at the and of "ContentResult" and replace "test/plain" with "text/plain" or better with "System.Net.Mime.MediaTypeNames.Text.Plain".Bucksaw
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.Crichton
@Christoph: I believe, it is part of system.dll msdn.microsoft.com/en-us/library/…Bucksaw
M
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" };
    }
}
Mckown answered 24/6, 2016 at 13:44 Comment(4)
I would also add exception handling based on type as well, so you can then determine the status code to return back as wellAntarctic
@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.Froth
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)); });Aberration
@Blake Rivell: Take a look at shabbirh's solution. That one shows how to modify the HTTP status code.Crichton
P
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.

Palmetto answered 27/5, 2017 at 8:10 Comment(5)
this is a custom solution, as it does not work for cases when code like _customersService.GetCustomerById(id); could throws an exception internally.Otoscope
true. it is just a simper, working version of the code in the original post.Palmetto
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.Palmetto
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 ?Kink
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.Ozonosphere
P
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);
}
Palacios answered 26/5, 2017 at 6:57 Comment(0)
F
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"
}
Funicular answered 12/8, 2021 at 13:28 Comment(0)
A
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.

Allover answered 19/6, 2019 at 0:46 Comment(1)
It is recommended NOT to wrap the HttpClient in a using statement. Better to make your HttpClient Static. aspnetmonsters.com/2016/08/2016-08-27-httpclientwrongAllover

© 2022 - 2024 — McMap. All rights reserved.