Search code examples
javaspring-boothibernatejpa

SpringBoot handle validation exceptions from API and JPA layer differently


I came upon a problem with validation exceptions on different layers.

If there is validation exception on Controller level (for example parameter has @Min value), Spring throws ConstraintViolationException. Since this is User fault, I map this to HTTP 400 error response.

But the same error is thrown by JPA on flushing entity if it has some constraints on entity fields. In this case, I cannot send 400 to user, since this is server fault and in many cases, these fields have nothing to do with data from user (they might, but not directly).

Is there a way to automatically wrap all exceptions from JPA level, so that ConstraintViolationException from controller (and possibly facade level) are not mixed with ConstraintViolationException from Hibernate/JPA level? Preferably for SpringBoot version 2.7.*

Thanks!


Solution

  • I decided to go this way. To decide if the constraint violation comes from Controller or JPA layer, I check the validated object found in the exception. In case of request parameters, this would be the Controller instance itself and I can distinguish those by checking if the class has @RestController annotation. Without probing the stackTrace.

    I could also go the other way around and check if the validated class is an @Entity. But this way seems more defensive, because I rather throw 500 in case of user error, than 400 in case of server error.

    @ExceptionHandler(ConstraintViolationException::class)
    fun handleValidationException(
      ex: ConstraintViolationException,
      response: HttpServletResponse
    ): ResponseEntity<ApiExceptionDto> {
      if (isControllerLevelValidationException(ex)) return createApiExceptionResponse(ex.localizedMessage, HttpStatus.BAD_REQUEST)
      logger.error("Unhandled internal error", ex);
      return createApiExceptionResponse("Internal Error", HttpStatus.INTERNAL_SERVER_ERROR)
    }
    
    
    private fun isControllerLevelValidationException(e: ConstraintViolationException): Boolean {
      val violations = e.constraintViolations
      for (violation in violations) {
        val rootBeanClass = violation.rootBeanClass
        if (!rootBeanClass.isAnnotationPresent(RestController::class.java)) return false
      }
      return true
    }