34

I have defined a pattern for validating email in my Entity class. In my validation exception handler class, I have added handler for ConstraintViolationException. My application utilize SpringBoot 1.4.5.

Profile.java

@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "profile")
public class Profile extends AuditableEntity {

  private static final long serialVersionUID = 8744243251433626827L;

  @Column(name = "email", nullable = true, length = 250)
  @NotNull
  @Pattern(regexp = "^([^ @])+@([^ \\.@]+\\.)+([^ \\.@])+$")
  @Size(max = 250)
  private String email;
....
}

ValidationExceptionHandler.java

@ControllerAdvice
public class ValidationExceptionHandler extends ResponseEntityExceptionHandler {

  private MessageSource messageSource;

  @Autowired
  public ValidationExceptionHandler(MessageSource messageSource) {
    this.messageSource = messageSource;
  }

  @ExceptionHandler(ConstraintViolationException.class)
  public ResponseEntity<Object> handleConstraintViolation(ConstraintViolationException ex,
  WebRequest request) {
    List<String> errors = new ArrayList<String>();
    ....
    }
} 

When I run my code and pass invalid email address, I get the following exception. The code in handleConstraintViolation is never executed. The http status returned in the exception is 500, but I want to return 400. Any idea how I can achieve that?

2017-07-12 22:15:07.078 ERROR 55627 --- [nio-9000-exec-2] o.h.c.s.u.c.UserProfileController        : Validation failed for classes [org.xxxx.common.service.user.domain.Profile] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
ConstraintViolationImpl{interpolatedMessage='must match "^([^ @])+@([^ \.@]+\.)+([^ \.@])+$"', propertyPath=email, rootBeanClass=class org.xxxx.common.service.user.domain.Profile, messageTemplate='{javax.validation.constraints.Pattern.message}'}]

javax.validation.ConstraintViolationException: Validation failed for classes [org.xxxx.common.service.user.domain.Profile] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
ConstraintViolationImpl{interpolatedMessage='must match "^([^ @])+@([^ \.@]+\.)+([^ \.@])+$"', propertyPath=email, rootBeanClass=class org.xxxx.common.service.user.domain.Profile, messageTemplate='{javax.validation.constraints.Pattern.message}'}]

at  org.hibernate.cfg.beanvalidation.BeanValidationEventListener.validate(BeanValidationEventListener.java:138)

at org.hibernate.cfg.beanvalidation.BeanValidationEventListener.onPreInsert(BeanValidationEventListener.java:78)    
Malathi
  • 2,119
  • 15
  • 40
bostonjava
  • 624
  • 1
  • 9
  • 20

12 Answers12

40

You cannot catch ConstraintViolationException.class because it's not propagated to that layer of your code, it's caught by the lower layers, wrapped and rethrown under another type. So that the exception that hits your web layer is not a ConstraintViolationException.

In my case, it's a TransactionSystemException. I'm using @Transactional annotations from Spring with the JpaTransactionManager. The EntityManager throws a rollback exception when somethings goes wrong in the transaction, which is converted to a TransactionSystemException by the JpaTransactionManager.

So you could do something like this:

@ExceptionHandler({ TransactionSystemException.class })
public ResponseEntity<RestResponseErrorMessage> handleConstraintViolation(Exception ex, WebRequest request) {
    Throwable cause = ((TransactionSystemException) ex).getRootCause();
    if (cause instanceof ConstraintViolationException) {
        Set<ConstraintViolation<?>> constraintViolations = ((ConstraintViolationException) cause).getConstraintViolations();
        // do something here
    }
}
nimai
  • 2,083
  • 1
  • 18
  • 16
  • 5
    Mind to comment on why the answer was downvoted? I understand the exact exception can be different in your case with your specific versions of frameworks (spring boot/hibernate/...), but the principle is the same and should apply for the person who asks. – nimai Jan 30 '18 at 18:18
  • 2
    This is the best answer so far. I noticed that they fixed this issue in Spring version >= 2.4.0 which means that you can now do this: `@ExceptionHandler(ConstraintViolationException.class)` & you're now able to handle all other nested exceptions too. – S34N Nov 15 '20 at 17:40
  • @nimai forgive them; for they know not what they do. Here, have an upvote. – nonNumericalFloat Jan 31 '22 at 16:08
6

Just want to add something. I was trying to do the same thing, validating the entity. Then I realized Spring has already everything out of the box if you validate the controller's input.

@RequestMapping(value = "/profile", method = RequestMethod.POST)
public ProfileDto createProfile(@Valid ProfileDto profile){
...    
}

The @Valid annotation will trigger the validation with the javax.validation annotations.

Suppose you have a Pattern annotation on your profile username with a regexp not allowing whitespaces.

Spring will build a response with status 400 (bad request) and a body like this one:

{
    "timestamp": 1544453370570,
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "Pattern.ProfileDto.username",
                "Pattern.username",
                "Pattern.java.lang.String",
                "Pattern"
            ],
            "arguments": [
                {
                    "codes": [
                        "profileDto.username",
                        "username"
                    ],
                    "arguments": null,
                    "defaultMessage": "username",
                    "code": "username"
                },
                [],
                {
                    "defaultMessage": "^[A-Za-z0-9_\\-.]+$",
                    "arguments": null,
                    "codes": [
                        "^[A-Za-z0-9_\\-.]+$"
                    ]
                }
            ],
            "defaultMessage": "must match \"^[A-Za-z0-9_\\-.]+$\"",
            "objectName": "profileDto",
            "field": "username",
            "rejectedValue": "Wr Ong",
            "bindingFailure": false,
            "code": "Pattern"
        }
    ],
    "message": "Validation failed for object='profileDto'. Error count: 1",
    "path": "/profile"
}
Ena
  • 3,481
  • 36
  • 34
5

Following solution is based on Spring Boot 2.1.2.

To clarify things... as nimai already correctly mentioned:

You cannot catch ConstraintViolationException.class because it's not propagated to that layer of your code, it's caught by the lower layers, wrapped and rethrown under another type. So that the exception that hits your web layer is not a ConstraintViolationException.

In your case it is probably a DataIntegrityViolationException, which points out a problem in the persistence layer. But you don't want to let it come that far.


Solution

Make use of the @Valid annotation for the entity given as method parameter as Ena mentioned. On my version it was missing the org.springframework.web.bind.annotation.RequestBody annotation (Without the @RequestBody annotation the ProfileDto cannot be parsed correctly into your ProfileDto entity and the properties are resulting in null values, e.g. NullPointerException.):

@RequestMapping(value = "/profile", method = RequestMethod.POST)
public ProfileDto createProfile(@Valid @RequestBody ProfileDto profile){
    ...
}

This will then return your wanted status code 400 and some default response body accompanied by a org.springframework.web.bind.MethodArgumentNotValidException before even reaching the persistence layer. The processing of the MethodArgumentNotValidException is defined in org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler.

This is another topic, but you then have the option to override that behaviour by creating a @ControllerAdvice with @ExceptionHandler(MethodArgumentNotValidException.class) and customize the response body to your needs, since the default error response body is not optimal and not even present when excluding ErrorMvcAutoConfiguration.

Caution: Locating the @ExceptionHandler(MethodArgumentNotValidException.class) inside the @ControllerAdvice that extends the ResponseEntityExceptionHandler results into an IllegalStateException, because in the ResponseEntityExceptionHandler already is an exception handler defined for MethodArgumentNotValidException. So just put it into another @ControllerAdvice class without extending anything.


Alternative manual approach

I saw you can also trigger the validation of the email pattern manually (see Manually call Spring Annotation Validation). I didn't test it myself, but I personally don't like that approach, because it is just bloating your controller code and I currently can't think of a use case that requires it.

I hope that helps others encountering a similar issue.

kluckow
  • 181
  • 1
  • 4
3

I would double check you've imported the right ConstraintViolationException

The one you want is from the org.hibernate.exception.ConstraintViolationException package. If you've imported the javax.validation.ConstraintViolationException it will be skipped as you've experienced.

import org.hibernate.exception.ConstraintViolationException;

@RestController
public class FeatureToggleController {

    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<Object> handleConstraintViolation(ConstraintViolationException ex, WebRequest request) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
    }
}

This will be called as expected.

Chris
  • 3,437
  • 6
  • 40
  • 73
  • Using org.hibernate.exception.ConstraintViolationException didn't work either. I have also encountered same issue but using org.hibernate.exception.ConstraintViolationException didn't solved my issue. – Ajay Mar 18 '20 at 14:35
2

You cannot catch ConstraintViolationException.class because it's not propagated to that layer of your code, it's caught by the lower layers, wrapped and rethrown under another type. So that the exception that hits your web layer is not a ConstraintViolationException. So you could do something like this:

@ExceptionHandler({TransactionSystemException.class})
protected ResponseEntity<Object> handlePersistenceException(final Exception ex, final WebRequest request) {
    logger.info(ex.getClass().getName());
    //
    Throwable cause = ((TransactionSystemException) ex).getRootCause();
    if (cause instanceof ConstraintViolationException) {        

        ConstraintViolationException consEx= (ConstraintViolationException) cause;
        final List<String> errors = new ArrayList<String>();
        for (final ConstraintViolation<?> violation : consEx.getConstraintViolations()) {
            errors.add(violation.getPropertyPath() + ": " + violation.getMessage());
        }

        final ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, consEx.getLocalizedMessage(), errors);
        return new ResponseEntity<Object>(apiError, new HttpHeaders(), apiError.getStatus());
    }
    final ApiError apiError = new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage(), "error occurred");
    return new ResponseEntity<Object>(apiError, new HttpHeaders(), apiError.getStatus());
}
1

Just check all Exceptions and select the one you need

  1. Need to determine the cause:

    while ((cause = resultCause.getCause()) != null && resultCause != cause) {
        resultCause = cause;
    }
    
  2. Use instanceof

    @ExceptionHandler(Exception.class)
    protected ResponseEntity<MyException> handleExceptions(Exception e) {
        String message;
        Throwable cause, resultCause = e;
        while ((cause = resultCause.getCause()) != null && resultCause != cause) {
            resultCause = cause;
        }
        if (resultCause instanceof ConstraintViolationException) {
            message = (((ConstraintViolationException) resultCause).getConstraintViolations()).iterator().next().getMessage();
        } else {
            resultCause.printStackTrace();
            message = "Unknown error";
        }
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new MyException(message));
    }
    
Zoe
  • 27,060
  • 21
  • 118
  • 148
gghnisan
  • 11
  • 1
  • 2
1

That is my solution...

@ExceptionHandler({DataIntegrityViolationException.class})
    protected ResponseEntity<Object> handlePersistenceException(final DataIntegrityViolationException ex) {

        Throwable cause = ex.getRootCause();

        if (cause instanceof SQLIntegrityConstraintViolationException) {

            SQLIntegrityConstraintViolationException consEx = (SQLIntegrityConstraintViolationException) cause;

            final ApiErrorResponse apiError =  ApiErrorResponse.newBuilder()
                    .message(consEx.getLocalizedMessage())
                    .status(HttpStatus.BAD_REQUEST)
                    .build();

            return new ResponseEntity<>(apiError, new HttpHeaders(), apiError.getStatus());
        }

        final ApiErrorResponse apiError =  ApiErrorResponse.newBuilder()
                .message(ex.getLocalizedMessage())
                .status(HttpStatus.NOT_ACCEPTABLE)
                .build();

        return new ResponseEntity<>(apiError, new HttpHeaders(), apiError.getStatus());
    }
@ExceptionHandler(RollbackException.class)
   public ResponseEntity<ApiErrorsListResponse> handleNotValidException(RollbackException ex){

       String errMessage = ex.getCause().getMessage();

       List<String> listErrMessage = getListErrMessage(errMessage);
       ApiErrorsListResponse response = ApiErrorsListResponse.newBuilder()
               .status(HttpStatus.NOT_ACCEPTABLE)
               .errorMessage(listErrMessage)
               .build();

       return new ResponseEntity<>(response, HttpStatus.NOT_ACCEPTABLE);

   }

    public static List<String> getListErrMessage(String msg){

        Stream<String> stream = Arrays.stream(msg.split("\n"))
                .filter(s -> s.contains("\t"))
                .map(s -> s.replaceAll("^([^\\{]+)\\{", ""))
                .map(s -> s.replaceAll("[\"]", ""))
                .map(s -> s.replaceAll("=", ":"))
                .map(s -> s.replaceAll("interpolatedMessage", "message"))
                .map(s -> s.replaceAll("\\{|\\}(, *)?", ""));

        return stream.collect(Collectors.toList());
    }
  • bean

public class ApiErrorsListResponse {

    private HttpStatus status;

 private List<String> errorMessage;

    public ApiErrorsListResponse() {
    }
...
}

skyho
  • 1,438
  • 2
  • 20
  • 47
1

Try this way..

@ControllerAdvice
public class ControllerAdvisor extends ResponseEntityExceptionHandler {

    @Autowired
    BaseResponse baseResponse;

    @ExceptionHandler(javax.validation.ConstraintViolationException.class)
    public ResponseEntity<BaseResponse> inputValidationException(Exception e) {

        baseResponse.setMessage("Invalid Input : " + e.getMessage());
        return new ResponseEntity<BaseResponse>(baseResponse, HttpStatus.BAD_REQUEST);

    }
}

arun
  • 11
  • 2
0

I think you should add @ResponseStatus(HttpStatus.BAD_REQUEST) to your @ExceptionHandler:

@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<Object> handleConstraintViolation(ConstraintViolationException ex, WebRequest request) {
    List<String> errors = new ArrayList<String>();
    ....
}
Abdullah Khan
  • 12,010
  • 6
  • 65
  • 78
Patrick
  • 12,336
  • 15
  • 73
  • 115
  • 1
    Did not work. For whatever reason Spring does not see the method handleConstraintViolation(). Another method (handleMethodArgumentNotValid()) in the same class get executed as it should - but not this method. – bostonjava Jul 13 '17 at 13:53
  • 1
    this will not work with some versions of SpringBoot. But it will work with Spring Boot version >= 2.4.0 – S34N Nov 15 '20 at 17:43
0
@ResponseBody
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
@ExceptionHandler(DataIntegrityViolationException.class)
public Map errorHandler(DataIntegrityViolationException ex) {
    Map map = new HashMap();
    map.put("rs_code", 422);
    map.put("rs_msg", "data existed !");
    return map;
}

just catch org.springframework.dao.DataIntegrityViolationException.

Zoe
  • 27,060
  • 21
  • 118
  • 148
0

You can handle org.hibernate.exception.ConstraintViolationException by adding this in your @controllerAdvice

@ExceptionHandler(DataIntegrityViolationException.class) public ResponseEntity handleConstraintViolationException(Exception ex){

    String errorMessage = ex.getMessage();
    errorMessage = (null == errorMessage) ? "Internal Server Error" : errorMessage;

    List<String> details = new ArrayList<>();
     details.add(ex.getLocalizedMessage());

    return new ResponseEntity<ErrorResponseDTO>(
            new ErrorResponseDTO( errorMessage ,details), HttpStatus.INTERNAL_SERVER_ERROR);

}
0

You can also catch ConstraintViolationException and throw own exception with @ResponseStatus code or another one and catch it in @ExceptionHandler(YourCustomException.class). If you want to do that you need to implements JpaRepository. During save you should to call saveAndFlush methods that means your code will be execute immediately in DB and you will be able to catch exception i try catch block. If you want, you can do it generic like that:

imports
...
public class ErrorHandler {

    public static <T> T execute(Supplier<T> repositorySaveFunction) {
        try {
            return repositorySaveFunction.get();
        } catch (DataIntegrityViolationException e) {
            if (e.getCause() instanceof org.hibernate.exception.ConstraintViolationException) {
                throw new CustomObjectAlreadyExistException();
            }
            if (e.getCause() instanceof PropertyValueException) {
                var fieldName = ((PropertyValueException) e.getCause()).getPropertyName();
                throw new CustomNotNullException(fieldName);
            }
            throw e;
        } catch (javax.validation.ConstraintViolationException e) {
            e.getConstraintViolations().forEach(constraintViolation -> {
                throw new CustomNotNullException(constraintViolation.getPropertyPath());
            });
            throw e;
        }
    }
}

Service:

        imports
    ...
        @Service
        @Transactional
        public class Service {
        
            private final YourRepository yourRepository;
            ... constructor
        
            public ObjectToSave save(ObjectToSave objectToSave) {
                return execute(() -> yourRepository.saveAndFlush(objectToSave));
            }

        }
mflorczak
  • 264
  • 3
  • 10