ASP.NET Core IExceptionHandler doesn't handle ValidationException thrown by attributes?

60 views Asked by At

I have the following DTO:

public record CreatorRequestTo(
    [property: JsonPropertyName("id")] 
    long Id,
    
    [property: JsonPropertyName("login")] 
    [StringLength(64, MinimumLength = 2)]
    string Login,
    
    [property: JsonPropertyName("password")] 
    [StringLength(128, MinimumLength = 8)]
    string Password,
    
    [property: JsonPropertyName("firstname")] 
    [StringLength(64, MinimumLength = 2)]
    string FirstName,
    
    [property: JsonPropertyName("lastname")] 
    [StringLength(64, MinimumLength = 2)]
    string LastName
);

When I try to send POST request with invalid data (password less than 8 characters), it throws ValidationException, which, for some reason, doesn't get handled by Exception Handler below:

public class GlobalExceptionHandler : IExceptionHandler
{

    public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
    {
        var errorCode = exception switch
        {
            EntityNotFoundException => (int)HttpStatusCode.BadRequest + "01",
            ValidationException => (int)HttpStatusCode.BadRequest + "02",
            _ => (int)HttpStatusCode.InternalServerError + "00"
        };
        var response = new ErrorResponseTo(exception.Message, errorCode);
        await httpContext.Response.WriteAsJsonAsync(response, cancellationToken);
        return true;
    }
}

What could be the reason? I want to send my own response, not ProblemDetails one with unnecessary data.

I get this as a response:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Password": [
      "The field Password must be a string with a minimum length of 8 and a maximum length of 128."
    ]
  },
  "traceId": "00-af83651d89556659fef605e3689da98f-cffd2070a3d9bc8f-00"
}

I want to get this as a response:

{
  "errorMessage": <exception message>,
  "errorCode": 40002
}

Program.cs file contains:

...
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
...
var app = builder.Build();
app.UseExceptionHandler();
1

There are 1 answers

0
Fengzhi Zhou On

The ExceptionHandler deals with exception , which is usually used for throw Exception(). Your need is a model-validation issue , and the validation runs before your exception , thus it is never hit.

The document has provided several solutions https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-8.0#exception-handler-page

  1. One workaround is that you can simply implement your custom error message through BadRequest :

Program.cs

builder.Services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressModelStateInvalidFilter = true;
});

Controller

    [HttpPost("test")]
    public IActionResult CreateCreator([FromBody] CreatorRequestDto creatorRequest)
    {
        if (!ModelState.IsValid)
        {
            //Select 1 Throw the exception
            //throw new Exception("XXXXX message from exception");
            //Select 2 return the badrequest
            return BadRequest(CreateValidationErrorResponse(ModelState));
        }

        return Ok("Creator created successfully.");
    }

    private object CreateValidationErrorResponse(ModelStateDictionary modelState)
    {
        var errors = modelState
            .Where(e => e.Value.Errors.Count > 0)
            .ToDictionary(
                e => e.Key,
                e => e.Value.Errors.Select(x => x.ErrorMessage).ToArray()
            );

        // Create your custom error response object
        return new
        {
            errorMessage = "Here BadRequest Validation failed.",
            errorCode = 40002,
            details = errors  //or you can hide
        };
    }

enter image description here

  1. Or you can throw the exception yourself

Program.cs

...
builder.Services.AddProblemDetails();
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressModelStateInvalidFilter = true;
});

var app = builder.Build();

app.UseExceptionHandler(x => x.Run(async context =>
{
    if (context.Response.HasStarted)
        return;

    var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
    if (exception == null)
        throw new Exception("ExceptionHandler without exception!");

    var problemDetailsService = context.RequestServices.GetService<IProblemDetailsService>();
    await problemDetailsService.WriteAsync(new ProblemDetailsContext
    {
        HttpContext = context,
        ProblemDetails =
        {
            Title = exception.Message,
            Status = int.Parse(context.Response.StatusCode.ToString() + "02")
        }
    });
}));
...

Controller

    [HttpPost("test")]
    public IActionResult CreateCreator([FromBody] CreatorRequestDto creatorRequest)
    {
        if (!ModelState.IsValid)
        {
            //Select 1 Throw the exception
            throw new Exception("XXXXX message from exception");
            //Select 2 return the badrequest
            //return BadRequest(CreateValidationErrorResponse(ModelState));
        }

        return Ok("Creator created successfully.");
    }

enter image description here