How to convert ConstraintViolationException 500 error to 400 bad request?
Asked Answered
S

3

29

If I use a constraint like this @NotNull and then in the controller

public User createUser(
            @Validated
            @RequestBody User user) {}

It gives a really nice 400 exception with details.

But if I use my own custom validator like this:

public User createUser(
            @UserConstraint
            @RequestBody User user) {}

It throws a 500 server error like this:

javax.validation.ConstraintViolationException: createUser.user: Error with field: 'test35'
    at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:117) ~[spring-context-5.1.10.RELEASE.jar:5.1.10.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.1.10.RELEASE.jar:5.1.10.RELEASE]
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:69) ~[spring-security-core-5.1.6.RELEASE.jar:5.1.6.RELEASE]

Is there a way to get nice 400 message to the response?

Ideally the 400 message should be the same as Spring's validation JSON

{
    "timestamp": "2019-10-30T02:33:15.489+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "Size.user.lastName",
                "Size.lastName",
                "Size.java.lang.String",
                "Size"
            ],
            "arguments": [
                {
                    "codes": [
                        "user.lastName",
                        "lastName"
                    ],
                    "arguments": null,
                    "defaultMessage": "lastName",
                    "code": "lastName"
                },
                25,
                1
            ],
            "defaultMessage": "size must be between 1 and 25",
            "objectName": "user",
            "field": "lastName",
            "rejectedValue": "",
            "bindingFailure": false,
            "code": "Size"
        }
    ],
    "message": "Validation failed for object='user'. Error count: 1",
    "path": "/api/v1/users"
}
Shaving answered 29/10, 2019 at 19:45 Comment(1)
Issue with solution: github.com/spring-projects/spring-boot/issues/…Grunion
N
17

Yes, you can create a custom error handler so you can add anything on your response and status as well. This is the simple way to change the status:

1.- Simple way to change status when ConstraintViolationException is thrown.

import javax.validation.ConstraintViolationException;

@ControllerAdvice
public class CustomErrorHandler {

    @ExceptionHandler(ConstraintViolationException.class)
    public void handleConstraintViolationException(ConstraintViolationException exception,
            ServletWebRequest webRequest) throws IOException {
        webRequest.getResponse().sendError(HttpStatus.BAD_REQUEST.value(), exception.getMessage());
    }
}    

2.- Custom way to put the response when a ConstraintViolationException occurs.

@ControllerAdvice
public class CustomErrorHandler {

    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<CustomError> handleConstraintViolationException(ConstraintViolationException exception) {
        CustomError customError = new CustomError();
        customError.setStatus(HttpStatus.BAD_REQUEST);
        customError.setMessage(exception.getMessage());
        customError.addConstraintErrors(exception.getConstraintViolations());
        return ResponseEntity.badRequest().body(customError);
    }
}   
Necker answered 29/10, 2019 at 20:1 Comment(6)
I want to format the JSON as the same as spring's JSON. Do you know how to do that? I've edited the question to show the formatShaving
Hi @erotsppa, sorry for the delay, do you still want to do that?Necker
@TheRealChx101 create a question so I can help you what you want to doNecker
Thanks. I found the solution. You have to arrange your parameters in a certain order: (@ModelAttribute Object, Errors, Model) so that spring doesn't throw the exceptionVickery
this is not a good solution, this will make all ConstraintViolationException return 400 when the question just wants the controller validation fail to return 400... it is important separate controller exceptions from datasource exceptions this solution brings a whole mess into exception handling for the whole projectDianthe
Joining to Rafael, this is a no brainer junior approach to take. You haven't considered anything, but to fix your problem in the shortest most straightforward way if you do this. An underlying DB validation could also bubble up to this point to return you http 400, while a constraint violation on that level are most commonly a coding error thus server error rather than being a client one. Do NOT use this approach in any circumstance.Heliotrope
S
13

As the solution above doesn't really produce the desired result here a link which might help: https://sterl.org/2020/02/spring-boot-hateoas-jsr303-validation/

Funny enough spring behaves differently if the class or the method request body is annotated with @Validated.

In other words on the class, you might encounter 500 errors. If you move the validation annotation, as you already did, into the method, the normal behavior should be 400.

Long story short, as soon as you have your custom contains, etc. you need to adjust the stuff a bit -- as in Spring, it is the MethodArgumentNotValidException and not the ConstraintViolationException, for which Spring already as a controller advice.

A quick solution may look like:

@Autowired
private MessageSource messageSource;

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ConstraintViolationException.class)
public @ResponseBody Map<String, Object> handleConstraintViolation(ConstraintViolationException e, ServletWebRequest request) {
    // emulate Spring DefaultErrorAttributes
    final Map<String, Object> result = new LinkedHashMap<>();
    result.put("timestamp", new Date());
    result.put("path", request.getRequest().getRequestURI());
    result.put("status", HttpStatus.BAD_REQUEST.value());
    result.put("error", HttpStatus.BAD_REQUEST.getReasonPhrase());
    result.put("message", e.getMessage());
    result.put("errors", e.getConstraintViolations().stream().map(cv -> SimpleObjectError.from(cv, messageSource, request.getLocale())));
    return result;
}

@Getter @ToString
static class SimpleObjectError {
    String defaultMessage;
    String objectName;
    String field;
    Object rejectedValue;
    String code;

    public static SimpleObjectError from(ConstraintViolation<?> violation, MessageSource msgSrc, Locale locale) {
        SimpleObjectError result = new SimpleObjectError();
        result.defaultMessage = msgSrc.getMessage(violation.getMessageTemplate(),
                new Object[] { violation.getLeafBean().getClass().getSimpleName(), violation.getPropertyPath().toString(),
                        violation.getInvalidValue() }, violation.getMessage(), locale);
        result.objectName = Introspector.decapitalize(violation.getRootBean().getClass().getSimpleName());
        result.field = String.valueOf(violation.getPropertyPath());
        result.rejectedValue = violation.getInvalidValue();
        result.code = violation.getMessageTemplate();
        return result;
    }
}
Sanctitude answered 8/5, 2020 at 11:54 Comment(1)
After e.getConstraintViolations().stream().map(...) you should collect Stream to List.Sexagenary
L
2

Simply, define a method annotated with @ExceptionHandler in a class annotated with @ControllerAdvice:

@ControllerAdvice
public class YourControllerAdvice {

    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(ConstraintViolationException.class)
    public void handleConstraintViolationException() {
    // Intentionally left blank
    }
}

Classes annotated with @ControllerAdvice are used to deal with exceptions at controller level.

Le answered 29/10, 2019 at 19:58 Comment(1)
If you left method blank - response body will be blank. Also, you can extend this advice for variety of exceptions: @ExceptionHandler(value = {ConstraintViolationException.class, ValidationException.class})Grunion

© 2022 - 2024 — McMap. All rights reserved.