springvalidationerror-handlingrequest-mappingcustom-error-handling

How can I make sure exceptions during parsing lead to the same kind of response as the (custom) response returned for validation failures?


I'm using Spring to create an API, but I'm having some trouble introducing custom error reporting on (a part of) the validation of the request body.

When parsing/validation errors occur, I want to give a custom response back to the user.
This works well for fields annotated with @Valid along with validators like @javax.validation.constraints.NotNull by using a custom ResponseEntityExceptionHandler annotated with @ControllerAdvice.
It does not work however if an Exception is thrown while parsing the request body (before the validations even run). In that case I get an html error page with status 500 (Server Error)

How can I make sure the exceptions during parsing lead to the same kind of response as the (custom) one I return for validation failures?


My endpoint's code looks like this:

@RequestMapping(value= "/endpoint"
    produces = { "application/json" }, 
    consumes = { "application/json" },
    method = RequestMethod.POST)
default ResponseEntity<Object> postSomething(@Valid @RequestBody MyRequestBody requestData){
    // ...
}

MyRequestBody class looks like this:

@Validated
public class MyRequestData {

    @JsonProperty("stringValue")
    private String stringValue = null;

    @NotNull
    @Valid
    public String getStringValue() {
        return stringValue;
    }

    // ...

    public enum EnumValueEnum {
        VALUE_1("value 1"),
        VALUE_1("value 2");

        private String value;

        EnumValueEnum(String value) {
            this.value = value;
        }

        @Override
        @JsonValue
        public String toString() {
            return String.valueOf(value);
        }

        @JsonCreator
        public static EnumValueEnum fromValue(String text) {
          if(text == null){
            return null;
          }
          for (EnumValueEnum b : EnumValueEnum.values()){
            if (String.valueOf(b.value).equals(text)) {
              return b;
            }
          }
          throw new HttpMessageNotReadableException("EnumValueEnum \"" + text + "\" does not exist");
        }

    }

    @JsonProperty("enumValue")
    private EnumValueEnum enumValue = null;
}

The custom validation error handling (and reporting) looks like this:

@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
public class MyValidationHandler extends ResponseEntityExceptionHandler {
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        // return status(BAD_REQUEST).body(new ValidationResponse(ex.getBindingResult().getFieldErrors()));
    }

    @Override
    protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        // return status(BAD_REQUEST).body(new ValidationResponse((JsonMappingException) ex.getCause()));
    }
}

In this code, if a user sends a request with an enum value that doesn't exist, an HttpMessageNotReadableException is thrown. I would like to catch that somewhere and replace it with a custom response that is consistent with the other exception handling I do. Where/How can I do that?


Solution

  • I found a solution to my own problem.

    You can actually use Spring MVC's normal exception handling:

    Annotating a method with @ExceptionHandler will make Spring try to use it for exception handling for the exception type specified (in the annotation's value field or the method's argument). This method can be placed in the controller or even in the ResponseEntityExceptionHandler I use for the other validation response handling.

    @ExceptionHandler
    public ResponseEntity handle(HttpMessageConversionException e){
        // return status(BAD_REQUEST).body(new ValidationResponse((JsonMappingException) e.getCause()));
    }
    

    Mind which type of exception you handle:

    The catch here was that the exception thrown while parsing is wrapped in (some subtype of) a JsonMappingException which in turn is wrapped again in a HttpMessageConversionException.

    e instanceof HttpMessageConversionException
    e.getCause() instanceof JsonMappingException
    e.getCause().getCause() // == your original exception
    

    The @ExceptionHandler should therefor accept HttpMessageConversionException instead of the originally thrown exception (which in my case was HttpMessageNotReadableException)

    It will not work if you write an @ExceptionHandler that only accepts your original Exception!