Tim
Tim

Reputation: 4284

How to catch ConstraintViolationExceptions in a REST Method

Before marking this as a duplicate: I read here and there that an ExceptionMapper will solve my problem, but for some reason it does not catch the ConstraintViolationException.

Update The problem is solved: Using a separate, more specific ExceptionMapper works (one that implements ExceptionMapper< ConstraintViolationException >). But I don't fully understand why a more general exception mapper (one that implements ExceptionMapper< Exception >) does NOT catch my ConstraintViolationException.


Original question:

I am introducing bean validation to my REST Methods:

@PUT
public Response updateUser(@NotNull @Valid UserUpdateDTO userUpdateDTO) {
    return ResponseUtil.ok(userService.updateUser(userUpdateDTO));
}

When a validation fails, I get a 400 response:

[PARAMETER]
[updateUser.arg0.initials]
[Initials must be between 3 and 5]
[AD]

I would like to catch the ConstraintViolationException before the response is sent because I have my own ResponseFactory.

Here is my ExceptionMapper (that works with my other exceptions!)

@Provider
public class ApiExceptionMapper implements ExceptionMapper<Exception> {

    @Override
    public Response toResponse(Exception e) {

        Throwable cause = (e instanceof EJBException) && e.getCause() != null ? e.getCause() : e;

        if (cause instanceof BadRequestException) {
            logger.error("BadRequest", cause);
            return ResponseUtil.badRequest(cause.getMessage());
        } else if (cause instanceof ForbiddenException) {
            logger.error("Forbidden", cause);
            return ResponseUtil.forbidden(cause.getMessage());
        } else if (cause instanceof ServerException) {
            logger.error("ServerException", cause);
            return ResponseUtil.serverError(cause.getMessage());
        } else if (cause instanceof ConstraintViolationException) {
            return ResponseUtil.badRequest("Validation failed");
        }

        // Default
        logger.error("unexpected exception while processing request", cause);
        return ResponseUtil.serverError(cause);
    }
}

The ExceptionMapper is not even called when a validation problem occurs, and I get the default 400 error right away.

What am I doing wrong ? I suspect that it has something to do with the fact that the exception is not thrown within the method's body, but rather in its signature.

I am using Wildfly 11 RC and its default validation

Upvotes: 1

Views: 2272

Answers (1)

Steve C
Steve C

Reputation: 19445

Given a Rest Service such as:

@Stateless
@Path("/people")
public class PersonService {

    @PersistenceContext(name = "people")
    private EntityManager em;

    @POST
    @Path("/")
    @Consumes(APPLICATION_JSON)
    public Response create(@Valid Person person) throws DuplicateKeyException {
        em.persist(person);
        return Response.created(UriBuilder.fromResource(PersonService.class)
                .path(PersonService.class, "getPerson")
                .resolveTemplate("id", person.getId()).build())
                .build();
    }

}

then the following ExceptionMapper works just fine by itself:

@Provider
public class ConstraintViolationExceptionMapper implements ExceptionMapper<ConstraintViolationException>{

    @Inject
    private Logger logger;

    private static class ConstraintViolationBean {
        private final String propertyName;
        private final String message;
        private final String invalidValue;

        private ConstraintViolationBean(ConstraintViolation constraintViolation) {
            final StringBuilder propertyPath = new StringBuilder();
            for (Path.Node node: constraintViolation.getPropertyPath()) {
                if (propertyPath.length() > 0) {
                    propertyPath.append('.');
                }
                propertyPath.append(node.getName());
            }
            this.propertyName = propertyPath.toString();
            this.message = constraintViolation.getMessage();
            this.invalidValue = constraintViolation.getInvalidValue().toString();
        }

        public String getPropertyName() {
            return propertyName;
        }

        public String getMessage() {
            return message;
        }

        public String getInvalidValue() {
            return invalidValue;
        }
    }

    @Override
    public Response toResponse(ConstraintViolationException exception) {
        logger.log(Level.WARNING, "Constraint violation: {}", exception.getMessage());
        List<ConstraintViolationBean> messages = new ArrayList<>();
        for (ConstraintViolation cv : exception.getConstraintViolations()) {
            messages.add(new ConstraintViolationBean(cv));
        }
        return Response.status(Response.Status.BAD_REQUEST)
                .entity(messages)
                .build();
    }

}

This is real working (not production) code that I have been messing with for fun. There is also an ExceptionMapper for the DuplicateKeyException.

You can find the source on github at jaxrs-people, which is essentially an experiment.

One thing I have noticed is that EJBExceptions seem to be unwrapped before the ExceptionMapper is selected and invoked.

Update:

Now, if I add the following implementation of ExceptionMapper<Exception> to the deployment, then this one is invoked and the remaining exception mappers are ignored.

@Provider
public class GenericExceptionMapper implements ExceptionMapper<Exception> {
    @Override
    public Response toResponse(Exception exception) {
        return Response.status(Response.Status.NOT_ACCEPTABLE)
                .build();
    }
}

Therefore it seems that because your ApiExceptionMapper is actually catching everything and your other ExceptionMappers will be ignored.

It looks like you need to either implement a separate ExceptionMapper for each of BadRequestException, ForbiddenException and ServerException, or some common ancestor class that is not Exception or RuntimeException.

I think that separate implementations would be better because code without if statements is easier to unit test.

What the Spec says:

§4.4 of "JAX-RS: Java™ API for RESTful Web Services (v2.0)" contains the statement:

When choosing an exception mapping provider to map an exception, an implementation MUST use the provider whose generic type is the nearest superclass of the exception.

This behaviour corresponds with what we have experienced here.

Upvotes: 2

Related Questions