I'd like to return a custom HTTP status 422 instead of a default 400 on a spring validation.
My validator:
#Component
#RequiredArgsConstructor
public class EmailUpdateDtoValidator implements Validator {
private Errors errors;
private EmailUpdateDto emailUpdateDto;
#Override
public boolean supports(Class<?> clazz) {
return EmailUpdateDto.class.equals(clazz);
}
#Override
public void validate(Object object, Errors errors) {
this.errors = errors;
this.emailUpdateDto = (EmailUpdateDto) object;
validateEmail();
}
private void validateEmail() {
if (!Email.isValid(emailUpdateDto.getEmail())) {
errors.rejectValue("email", UserValidationErrorCodes.EMAIL_NOT_VALID.name());
}
}
}
How I setup the validation in the Controller:
#Slf4j
#RestController
#RequiredArgsConstructor
public class UserController {
private final EmailUpdateDtoValidator emailUpdateDtoValidator;
#InitBinder("emailUpdateDto")
protected void initEmailValidationBinder(final WebDataBinder binder) {
binder.addValidators(emailUpdateDtoValidator);
}
#RequestMapping(value = "/users/{hashedId}/email", method = RequestMethod.PUT)
public void updateEmail(#RequestBody #Valid EmailUpdateDto emailUpdateDto) {
...
}
}
Using this setup I always get a 400. How could I customize the HTTP status on the return?
Thanks
The validation process would throw an org.springframework.web.bind.MethodArgumentNotValidException, therefore you can add an exception handler to your controller:
import org.springframework.web.bind.MethodArgumentNotValidException;
#ExceptionHandler
public ResponseEntity<String> handleException(MethodArgumentNotValidException ex) {
return new ResponseEntity<String>(HttpStatus.UNPROCESSABLE_ENTITY);
}
As workaround you can define a ExceptionHandler and override the default behavior.
#ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<Object> customHttpStatus() {
return ResponseEntity.status(422).build();
}
}
Related
I have a REST-Backend created with JHipster. There are different exception-classes in the service layer and the web-rest layer. This service-exceptions are translated by an ExceptionTranslator which implements the ProblemHandling interface from org.zalando.problem.spring.web.advice
I have the following ExceptionTranslator:
#ControllerAdvice
public class ExceptionTranslator implements ProblemHandling, SecurityAdviceTrait {
#Override
public ResponseEntity<Problem> process(#Nullable ResponseEntity<Problem> entity, NativeWebRequest request) {
//generated by jHipster
}
#ExceptionHandler(HouseWithoutOwnerServiceException.class)
public ResponseEntity<Problem> handleHouseWithoutOwnerException(HouseWithoutOwnerServiceException ex, NativeWebRequest request) {
return create(new HouseWithoutOwnerException(), request);
}
}
The service-exception class:
public class HouseWithoutOwnerServiceException extends RuntimeException {
public HouseWithoutOwnerServiceException() {
super("House without owner!");
}
}
The rest-error class:
public class HouseWithoutOwnerException extends AbstractThrowableProblem {
private static final long serialVersionUID = 1L;
public HouseWithoutOwnerException() {
super(ErrorConstants.HOUSE_WITHOUT_OWNER_TYPE, "House does not have an owner", Status.CONFLICT);
}
}
In my test the HouseWithoutOwnerServiceException is thrown but not translated into a HouseWithoutOwnerException:
#SpringBootTest(classes = HouseApp.class)
public class HouseControllerIT {
#Autowired
private MappingJackson2HttpMessageConverter jacksonMessageConverter;
#Autowired
private PageableHandlerMethodArgumentResolver pageableArgumentResolver;
#Autowired
private ExceptionTranslator exceptionTranslator;
private MockMvc restHouseMockMvc;
#BeforeEach
public void setup() {
HouseController houseController = new HouseController(houseService);
this.restHouseMockMvc = MockMvcBuilders.standaloneSetup(houseController)
.setCustomArgumentResolvers(pageableArgumentResolver)
.setControllerAdvice(exceptionTranslator)
.setMessageConverters(jacksonMessageConverter)
.build();
}
#Test
#Transactional
public void createHouseWithoutExistingOwner() throws Exception {
HouseDTO houseDTO = createHouseDTOWithoutOwner();
houseDTO.setOwnerId(ownerId + 1); //not existing
restHouseMockMvc.perform(post("/api/v1/houses")
.contentType(TestUtil.APPLICATION_JSON_UTF8)
.content(TestUtil.convertObjectToJsonBytes(houseDTO)))
.andExpect(status().isConflict());
}
}
Therefore I always get 500 Internal Server Error instead of 409 Conflict. I debugged it already and the method in the ExceptionTranslator is not entered.
toResponse method of ExceptionMapper class is not invoke.
Code
public class PortalExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
#Override
public Response toResponse(ConstraintViolationException exception) {
System.out.println("%%%%%%%%%%%%%%%%%%%%%%%% testing product%%%%%%%%%%%%%%%%%%%%%%%%%%%%");
return Response.status(Response.Status.BAD_REQUEST)
.entity(prepareMessage(exception))
.type("application/json")
.build();
}
private String prepareMessage(ConstraintViolationException exception) {
String msg = "";
for (ConstraintViolation<?> cv : exception.getConstraintViolations()) {
msg+=cv.getPropertyPath()+" "+cv.getMessage()+"\n";
}
return msg;
}
}
=======================================================
This exceptionMapper class register on ResourcesConfig
public class PortalApp extends ResourceConfig
{
public PortalApp()
{
register(AccessRequestFilter.class);
packages("org.learn");
register(JacksonFeature.class);
register(PortalExceptionMapper.class);
register(PortalJackson.class);
register(PortalFilter.class);
}
Please help. Thanks
Try to mark your class with #Provider:
#Provider
public class PortalExceptionMapper implements ExceptionMapper<ConstraintViolationException> {...}
I'm trying to use the Spring Validator and #Validated annotation to validate a Get Request parameter but cannot get the validator to run. I'm using a ModelAttribute to try and get the validator to run on the Path Variable instead of the Request Body. Is it possible to run a validator on a Get Request Path Variable?
Here is my controller class and method
#RestController
public class ProfileController {
#RequestMapping(value = "/profile/{param}", method = RequestMethod.GET)
public IVRProfile getProfile(#Validated(ParamValidator.class) #ModelAttribute("param") String param) {
return sampleProfile();
}
#ModelAttribute("param")
public String paramAsModelAttribute(#PathVariable String param) {
return param;
}
}
And the Validator class
#Component
public class ParamValidator implements Validator
{
#Override
public boolean supports(Class<?> clazz)
{
System.out.println("Validator supports test");
return String.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors)
{
System.out.println("Validator Test");
// Validation code
}
}
Neither prints statements are executed when hitting the endpoint.
Any help on what I could be missing or do differently would be greatly appreciated, thanks!
You can implement desired validation functionality as following.
public class ParamValidator implements ConstraintValidator<ParamConstraint, String> {
#Override
public void initialize(ParamConstraint paramConstraint) {
}
#Override
public boolean isValid(String paramField, ConstraintValidatorContext cxt) {
//Perform paramField validation
return true;
}
}
-
#Documented
#Constraint(validatedBy = ParamValidator.class)
#Target( { ElementType.PARAMETER })
#Retention(RetentionPolicy.RUNTIME)
public #interface ParamConstraint {
String message() default "Default validation message";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
-
#RequestMapping(value = "/profile/{param}", method = RequestMethod.GET)
public IVRProfile getProfile(#Valid #ParamConstraint #ModelAttribute("param") String param) {
return sampleProfile();
}
And finally don't forget to annotate Controller with #Validated.
#RestController
#Validated
public class ProfileController {
//...
}
More details you can find in the example as mentioned here.
You can create the answer you want by using the fields in the ConstraintViolationException with the following method;
#ExceptionHandler(ConstraintViolationException.class)
protected ResponseEntity<Object> handlePathVariableError(final ConstraintViolationException exception) {
log.error(exception.getMessage(), exception);
final List<SisSubError> subErrors = new ArrayList<>();
exception.getConstraintViolations().forEach(constraintViolation -> subErrors.add(generateSubError(constraintViolation)));
final SisError error = generateErrorWithSubErrors(VALIDATION_ERROR, HttpStatus.BAD_REQUEST, subErrors);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
You need to added an #Validated annotation to Controller class and any validation annotation before path variable field
If you want to get single RequestParams like status, you can force it by following the code below.
#RestController
public class ProfileController {
#RequestMapping(value = "/profile/{param}", method = RequestMethod.GET)
public IVRProfile getProfile(#RequestParam(name = "status", required = true) String status, #ModelAttribute("param") String param) {}
}
if you want to force PathVariable, then do this.
#RestController
public class ProfileController {
#RequestMapping(value = "/profile/{param}", method = RequestMethod.GET)
public IVRProfile getProfile(#PathVariable(name = "param", required = true) String param, #ModelAttribute("param") String param) {}
}
Hope this work!!!
I have a test case where am throwing exception incase of some basic validation. but ExceptionMapper is not being invoked. But if i run from postman to hit the service it is working fine.
Do Junit test have to run differently for ExceptionMapper ?
Test case :
#Test
public void itShouldHavePersonNumber() {
RestAuthController controller = new RestAuthController();
Response response = controller.insertGuid(null, "m012");
assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> {controller.insertGuid(null, "m012");});
assertThat(response.getStatus()).isEqualTo(Status.BAD_REQUEST.getStatusCode());
}
Controller:
#POST
#Consumes(MediaType.APPLICATION_JSON)
public Response insertGuid(#QueryParam("personNumber") Integer personNumber, #QueryParam("guId") String guId ) throws ValidationException {
if(guId == null || guId.isEmpty()) {
throw new ValidationException("guId is Required");
}
}
Exception Mapper :
#Provider
public class ValidationMapper implements ExceptionMapper<ValidationException> {
#Override
public Response toResponse(ValidationException ex) {
return Response.status(Response.Status.BAD_REQUEST).entity(ex.getMessage()).type(MediaType.TEXT_PLAIN).build();
}
}
Exception:
public class ValidationException extends Exception {
/**
*
*/
private static final long serialVersionUID = 1L;
public ValidationException() {
super();
}
public ValidationException(String message, Throwable cause) {
super(message, cause);
}
public ValidationException(String message) {
super(message);
}
}
Why do you think the exception mapper should be called? It is not an integration test. All you are doing is instantiating the class and then calling a method. There is nothing magical in Java that will make the exception mapper be called. You need to run an integration test with the Jersey application running (and the mapper registered) if you want the mapper to be called.
One way to run an integration test with Jersey is to use it's Test Framework. Below is an example.
public class ValidationExceptionTest extends JerseyTest {
public static class ValidationException extends RuntimeException {}
public static class ValidationExceptionMapper implements ExceptionMapper<ValidationException> {
#Override
public Response toResponse(ValidationException e) {
return Response.status(400).entity("boo boo").build();
}
}
#Path("echo-name")
public static class EchoNameResource {
#GET
public String echoName(#QueryParam("name") String name) {
if (name == null || name.isEmpty()) {
throw new ValidationException();
}
return name;
}
}
#Override
public ResourceConfig configure() {
return new ResourceConfig()
.register(EchoNameResource.class)
.register(ValidationExceptionMapper.class);
}
#Test
public void testResponseOkWithQueryParam() {
final Response response = target("echo-name")
.queryParam("name", "peeskillet")
.request()
.get();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.readEntity(String.class)).isEqualTo("peeskillet");
}
#Test
public void testResponseBadRequestWithNoQueryParam() {
final Response response = target("echo-name")
.request()
.get();
assertThat(response.getStatus()).isEqualTo(400);
}
}
We have a problem with our tests that the field UriInfo is not correctly injected when the resource is wrapped in a TransactionalProxy.
We tried using the SpringResourceFactory but that did not help either.
I tried to extract the relevant classes for this usecase:
public class InMemoryClientFactory implements FactoryBean<InMemoryClientExecutor>{
#Inject
private SessionResource sessionResource;
#Override
public InMemoryClientExecutor getObject() throws Exception {
Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
Registry registry = dispatcher.getRegistry();
registry.addSingletonResource(sessionResource);
final InMemoryClientExecutor inMemoryClientExecutor = new InMemoryClientExecutor(dispatcher);
}
#Override
public Class getObjectType() {
return InMemoryClientExecutor.class;
}
#Override
public boolean isSingleton() {
return true;
}
}
#Path("session")
public interface SessionResource {
#GET
#Path("{sessionId}")
#Produces({MediaType.APPLICATION_XML})
Response get(#PathParam("sessionId") String sessionId);
#DELETE
#Path("{sessionId}")
Response delete(#PathParam("sessionId") String sessionId);
}
#Service
#Transactional
public class SessionResourceImpl implements SessionResource {
#Context
private UriInfo uriInfo;
#Override
public Response get(String sessionId) {
// uriInfo will be null here
String url = uriInfo.getBaseUriBuilder().path(SessionResource.class).path(SessionResource.class, "delete").build(sessionId)
.toString());
return Response.ok(session).build();
#Override
public Response delete(String sessionId) {
System.out.println("Deleted Session "+1);
}
}
#ContextConfiguration(locations = ["classpath:/META-INF/testContext.xml"])
#Transactional
#RunWith(SpringJUnit4ClassRunner.class)
public class SessionResourceIT {
#Inject
InMemoryRestClientFactory inMemoryClientFactory;
#Inject
SessionResource resource;
#Test
public void test() {
SessionResource resource = inMemoryClientFactory.createProxy(SessionResource.class);
ClientResponse cr = client.get(sessionId);
assertNotNull(cr.getEntity(String.class));
}
}
A possible workaround is to unwrap the transactional proxy for the tests, this works as long as the test itself is annotated with #Transactional. I hope someone has a better solution than this.
public class InMemoryClientFactory implements FactoryBean<InMemoryClientExecutor>{
#Inject
private SessionResource sessionResource;
#Override
public InMemoryClientExecutor getObject() throws Exception {
Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
Registry registry = dispatcher.getRegistry();
registry.addSingletonResource(unwrapProxy(sessionResource));
final InMemoryClientExecutor inMemoryClientExecutor = new InMemoryClientExecutor(dispatcher);
}
#Override
public Class getObjectType() {
return InMemoryClientExecutor.class;
}
#Override
public boolean isSingleton() {
return true;
}
private static Object unwrapProxy(Object bean) throws Exception {
Object result = bean;
/*
* If the given object is a proxy, set the return value as the object
* being proxied, otherwise return the given object.
*/
if (AopUtils.isAopProxy(bean) && bean instanceof Advised) {
Advised advised = (Advised) bean;
result = advised.getTargetSource().getTarget();
}
return result;
}
}