proudmonkey / AutoWrapper

A simple, yet customizable global exception handler and Http response wrapper for ASP.NET Core APIs.
MIT License
677 stars 82 forks source link

Inconsistent type for exceptionMessage (AutoWrapper 4.5) #133

Open nwoolls opened 2 years ago

nwoolls commented 2 years ago

Hi there. First of all, thanks for your time and project!

I am having an issue with the latest stable version of the library (4.5) running on .NET Core 6.0.

Specifically, under some circumstances the exceptionMessage JSON property is a string, and in some cases the exceptionMessage JSON property is an object. This poses issues when parsing the result, especially using C# where we expect to be able to map the property to a specific type.

You can reproduce this with the following steps.

From the command line:

dotnet --version
6.0.101
mkdir autowrapper-exceptionmessage-issue
cd autowrapper-exceptionmessage-issue/
dotnet new webapi
dotnet add package AutoWrapper.Core --version 4.5.0

Edit Program.cs and add:

var options = new AutoWrapperOptions
{
    IsDebug = app.Environment.IsDevelopment()
};
app.UseApiResponseAndExceptionWrapper(options);

Edit WeatherForecastController.cs and add:

public class InputModel
{
    [Required(AllowEmptyStrings = false)]
    public string? FirstName { get; set; }
}

[HttpPost]
public IActionResult Post(InputModel inputModel)
{
    if (inputModel.FirstName == "foo")
    {
        return StatusCode(StatusCodes.Status400BadRequest, "An error occurred - returning status code");
    }

    if (inputModel.FirstName == "bar")
    {
        throw new ApiException("An error occurred - throwing ApiException");
    }

    return StatusCode(StatusCodes.Status201Created);
}

Run the project.

The following are examples where exceptionMessage may either be an object or a string:

Example 1 - exceptionMessage is an object

curl -X 'POST' \
  'https://localhost:7284/WeatherForecast' \
  -H 'accept: */*' \
  -H 'Content-Type: application/json' \
  -d '{}'
{
  "isError": true,
  "responseException": {
    "exceptionMessage": {
      "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
      "title": "One or more validation errors occurred.",
      "status": 400,
      "traceId": "00-12ce2cc5118d85bf077d9d4f71342ef0-23dae6d83195f904-00",
      "errors": {
        "FirstName": [
          "The FirstName field is required."
        ]
      }
    }
  }
}

Example 2 - exceptionMessage is an object

curl -X 'POST' \
  'https://localhost:7284/WeatherForecast' \
  -H 'accept: */*' \
  -d '{
  "firstName": "string"
}'
{
  "isError": true,
  "responseException": {
    "exceptionMessage": {
      "type": "https://tools.ietf.org/html/rfc7231#section-6.5.13",
      "title": "Unsupported Media Type",
      "status": 415,
      "traceId": "00-bc5f37b0710ad9189d09f5de6cfdd981-59a78671fdb1671e-00"
    }
  }
}

Example 3 - exceptionMessage is a string

curl -X 'POST' \
  'https://localhost:7284/WeatherForecast' \
  -H 'accept: */*' \
  -H 'Content-Type: application/json' \
  -d '{
  "firstName": "foo"
}'
{
  "isError": true,
  "responseException": {
    "exceptionMessage": "An error occurred - returning status code"
  }
}

Example 4 - exceptionMessage is a string

curl -X 'POST' \
  'https://localhost:7284/WeatherForecast' \
  -H 'accept: */*' \
  -H 'Content-Type: application/json' \
  -d '{
  "firstName": "bar"
}'
{
  "isError": true,
  "responseException": {
    "exceptionMessage": "An error occurred - throwing ApiException"
  }
}

Any guidance on this would be very much appreciated. Thanks in advance!

Varveyn commented 1 year ago

I've managed to workaround this issue in case of 400 Bad Request by using custom error model together with overriding the ApiBehaviorOptions.InvalidModelStateResponseFactory.

Something like this:

In Program.cs:

_ = builder.Services
    .Configure<ApiBehaviorOptions>(options => options
        .InvalidModelStateResponseFactory = CustomInvalidModelStateResponseFactory.OnActionExecuting);

In CustomInvalidModelStateResponseFactory.cs:

// trimmed for brevity
public static IActionResult OnActionExecuting(ActionContext context)
{
    // trimmed for brevity
    throw new ApiException(new GenericApiError
    {
        error_message = "Request model is invalid.",
        validation_errors = validationErrors,
    });
}