Adam Ostrožlík
Adam Ostrožlík

Reputation: 1416

ConstraintViolationException - distinguish between spring a hibernate for HttpStatus

I have custom ConstraintValidator which checks @RequestParam for valid values. I also have hibernate entities with some attributes annotated with @NotNull.

Default behavior is that ConstraintViolationException is translated to HttpStatus=500. This is desired for error raising from hibernate (that is persisting entities and so). But I would like to raise HttpStatus=400 for ConstraintViolations raised from @RestController.

@ExceptionHandler works but it cannot distinguish between violations raised from hibernate and spring layer. I guess both layers uses hibernate validator dependency but we should be able to distinguish among those.

 @ExceptionHandler(ConstraintViolationException.class)
    protected ResponseEntity<Object> handleConstraintViolationException(ConstraintViolationException ex, WebRequest request) {
        return getObjectResponseEntity(ex, request, HttpStatus.BAD_REQUEST);
    }

I wonder if there are some other ways to achieve this. I have tried @InitBinder validator but it seems not to work with @RequestParam.

Do you have any ideas? Thanks.

One solution for this is to list all violations from the given exception and check if the root bean is names like *Controller but this is not quite clean solution and bound a developer to name controllers this way.

@ExceptionHandler(ConstraintViolationException.class)
protected ResponseEntity<Object> handleConstraintViolationException(ConstraintViolationException ex, WebRequest request) {
    // TODO ugly
    boolean isFromController = ex.getConstraintViolations().stream()
        .map(cv -> cv.getRootBeanClass().getSimpleName())
        .anyMatch(cv -> cv.contains("Controller"));
    return getObjectResponseEntity(ex, request, isFromController ? HttpStatus.BAD_REQUEST : HttpStatus.INTERNAL_SERVER_ERROR);
}

The controller looks like this with the @SupportedFileTypes custom validator which checks string value against internal enum.

@GetMapping(value = "/{requirementId}/export-cisel", produces = {MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE})
public CisRadDto exportRequirementNumbers(
    @PathVariable UUID requirementId,
    @SupportedFileTypes @RequestParam(value = "formatSouboru", required = false) String fileFormat
) {
    return pozadavekFacade.exportRequirementNumbers(requirementId, fileFormat != null ? fileFormat : "default");
}

pozadavekFacade only saves the created entity which has the folowing check:

@NotBlank
@Column(length = 10, nullable = false)
private String idUzivatele;

Upvotes: 0

Views: 331

Answers (1)

CodeScale
CodeScale

Reputation: 3314

As your code is spread across different layers you should be able to catch ConstraintViolationException in each of them.

So in your facade you can catch it and rethrow them as RuntimeException (or whatever the exception you want). Then your controller layer will map this exception to 500 internal error.

And in your controller layer keep your logic and translate ConstraintViolationException to a 400 bad request response.

Updated : you're not forced to add try-catch boilerplate code everywhere.

Example : (don't copy paste this code. It is just to demonstrate how you can get rid of try-catch block at multiple places)

class Service {

    String a() {throw new RuntimeException();}
    String b() {throw new RuntimeException();}
    String c() {throw new RuntimeException();}
}
class Facade {
    Service service;

    String a() {return tryRun(() -> service.a());}
    String b() {return tryRun(() -> service.b());}
    String c() {return tryRun(() -> service.c());}

    String tryRun(Supplier<String> run) {
        try {
            return run.get();
        }
        catch(ConstraintViolationException e) {
            throw new RuntimeException(""); // don't forget to extract useful information from ConstraintViolationEx type.
        }
    }

}

Upvotes: 1

Related Questions