I am sending a DTO from client with a failed validation for one field. I tried to handle it in the Controller Advice, but it turns out it is not the instance of the ValidationException.class as that handler does not work. I checked it using the #ExceptionHandler(Exception.class) and it worked as expected, but I want to specify the Exception type properly.
This is the DTO:
public class BookDto {
#NotBlank(message = "Name of the book cannot be empty!")
#Size(min = 4, max = 30, message = "Name of the book must be between 4 and 30 characters
long!")
public String name;
#NotBlank(message = "Author of the book cannot be empty!")
#Size(min = 4, max = 35, message = "Author of the book must be between 5 and 35
characters long!")
public String author;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
public LocalDate publicationYear;
#NotNull(message = "Pages of the book cannot be null!")
#Digits(integer = 4, fraction = 0)
public int pagesNumber;
#NotBlank(message = "Publisher of the book cannot be empty!")
#Size(min = 4, max = 30, message = "Publisher of the book must be between 4 and 30
characters long!")
public String publisher;
}
This is the ControllerAdvice:
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(ResourceNotFoundException.class)
protected ResponseEntity<?> resourceNotFoundException(ResourceNotFoundException ex,
WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(),
request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
#ExceptionHandler(ResourceWasDeletedException.class)
protected ResponseEntity<MyGlobalExceptionHandler> handleDeleteException() {
return new ResponseEntity<>(new MyGlobalExceptionHandler("This book was deleted!"),
HttpStatus.NOT_FOUND);
}
#ExceptionHandler(ValidationException.class)
public ResponseEntity<?> handleValidationException(ValidationException exception,
WebRequest webRequest) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), exception.getMessage(),
webRequest.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
}
/* #ExceptionHandler(Exception.class)
public ResponseEntity<?> globalExceptionHandler(Exception ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(),
request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
}*/
#Data
#AllArgsConstructor
private static class MyGlobalExceptionHandler {
private String message;
}
}
This is the post method:
#PostMapping("/books")
#ResponseStatus(HttpStatus.CREATED)
public BookDto createBook(#Valid #RequestBody BookDto requestForSave){
var book = converter.getMapperFacade().map(requestForSave, Book.class);
log.debug("Book={}", book);
var bookDto = converter.toDto((Book) bookService.create(book));
log.debug("Book={}", bookDto);
return bookDto;
}
When I commented the last Exception handler Postman receives a response with this:
So the ValidationException handler did not work. What type of the Exception is this?
You can figure that out yourself: Restore your commented-out catch-all exception handler and just print or otherwise report ex.getClass(). exceptions are objects same as any other. They have a type, you can instanceof them, you can call .getClass() on them, you can pass them around like any other.
Related
I recently implemented authentication in my API Rest, but when the user tries to access to api without being auntenticatedn the exception message doesn't appear:
This is my GlobalExceptionHandler with the method exception that cover all errors in the api:
#ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(Exception.class)
public ResponseEntity<ErrorDetails> handlerExceptionMethod(Exception ex , WebRequest
webRequest){
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(),
webRequest.getDescription(false));
return new ResponseEntity<ErrorDetails>(errorDetails,
HttpStatus.INTERNAL_SERVER_ERROR);
}
The Postman sofware returns an 1, I want to see the errorDetails parameters. This is my ErrorDetails class:
#Getter
public class ErrorDetails {
private Date timestamp;
private String message;
private String details;
public ErrorDetails(Date timestamp, String message, String details) {
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
}
I solved the problem by addding the following line of code in Security COnfig class:
.antMatchers("/error").permitAll()
I want to test my StudentDTO :
#Entity
#ToString
#Setter
#Getter
#NoArgsConstructor
#AllArgsConstructor
public class StudentDTO {
#Id
private int studentId;
#NotNull
#Size(min=2,max=30,message = "Name should consist of 2 to 30 symbols!")
private String studentName;
#NotNull
#Size(min = 2, max = 30,message = "Surname should consist of 2 to 30 symbols!")
private String studentSurname;
#NotNull
#Min(value = 10,message = "Student age should be more than 10!")
private int studentAge;
#NotNull
#Min(value = 1900,message = "Entry year should be more than 1900!")
#Max(value=2021,message = "Entry year should be less than 2021!")
private int entryYear;
#NotNull
#Min(value = 2020,message = "Graduate year should be not less than 2020!")
private int graduateYear;
#NotNull
#Size(min = 3,message = "Faculty name should consist of minimum 3 symbols!")
private String facultyName;
#NotNull
#Size(min = 4,message = "Group name should consist of 4 symbols!")
#Size(max = 4)
private String groupName;
}
Method for testing in StudentController :
#PostMapping("successStudentAddition")
public String addStudent(#ModelAttribute("student") #Valid StudentDTO studentDTO, Errors errors, Model model) {
if (errors.hasErrors()) {
model.addAttribute(STUDENT_MODEL, studentDTO);
return "/studentViews/addStudent";
}
Student student = new Student(studentDTO.getStudentId(), studentDTO.getStudentName(), studentDTO.getStudentSurname(),
studentDTO.getStudentAge(), studentDTO.getEntryYear(), studentDTO.getGraduateYear(), studentDTO.getFacultyName(),
groupService.getGroupIdByName(studentDTO.getGroupName()));
studentService.addStudent(student);
return "/studentViews/successStudentAddition";
}
I am trying to test in this way :
#ExtendWith(SpringExtension.class)
#WebMvcTest(controllers = StudentController.class)
class StudentControllerTest {
#Autowired
private MockMvc mvc;
#Autowired
private ObjectMapper objectMapper;
#MockBean
private StudentController studentController;
#Test
void whenInputIsInvalid_thenReturnsStatus400() throws Exception {
StudentDTO studentDTO = new StudentDTO();
studentDTO.setStudentId(0);
studentDTO.setStudentName("Sasha");
studentDTO.setStudentSurname("Georginia");
studentDTO.setStudentAge(0);
studentDTO.setEntryYear(5);
studentDTO.setGraduateYear(1);
studentDTO.setFacultyName("facop");
studentDTO.setGroupName("BIKS");
mvc.perform(post("/studentViews/successStudentAddition")
.accept(MediaType.TEXT_HTML))
.andExpect(status().isBadRequest())
.andExpect(model().attribute("student", studentDTO))
.andDo(print());
}
}
In my test I got 200 error, but I need to get 400 error with determined error above on the field from my StudentDTO.
e.g. if I pass studentAge = 5, I should to get 400 error and the message : Student age should be more than 10! like in the StudentDTO.
When you have such a condition, Spring will throw MethodArgumentNotValidException. To handle these exceptions you can write a class with #ControllerAdvice.
#ControllerAdvice
public class ErrorHandler {
#ExceptionHandler(value = {MethodArgumentNotValidException.class})
public ResponseEntity<Error> invalidArgumentExceptionHandler(MethodArgumentNotValidException ex) {
// Instead of "/studentViews/successStudentAddition" you can return to some generic error page.
return new ResponseEntity<String>("/studentViews/successStudentAddition", HttpStatus.BAD_REQUEST);
}
}
I often turn to spring's org.springframework.http.ResponseEntity class.
#PostMapping("successStudentAddition")
public ResponseEntity<String> addStudent(#ModelAttribute("student") #Valid StudentDTO studentDTO, Errors errors, Model model) {
if (errors.hasErrors()) {
model.addAttribute(STUDENT_MODEL, studentDTO);
return new ResponseEntity<String>("/studentViews/addStudent", HttpStatus.BAD_REQUEST);
}
Student student = new Student(studentDTO.getStudentId(), studentDTO.getStudentName(), studentDTO.getStudentSurname(),
studentDTO.getStudentAge(), studentDTO.getEntryYear(), studentDTO.getGraduateYear(), studentDTO.getFacultyName(),
groupService.getGroupIdByName(studentDTO.getGroupName()));
studentService.addStudent(student);
return new ResponseEntity<String>("/studentViews/successStudentAddition", HttpStatus.Ok);
}
I have the following REST endpoints in Spring boot
#GetMapping(value = "students", params = {"name"})
public ResponseEntity<?> getByName(#RequestParam final String name) {
return new ResponseEntity<>(true, HttpStatus.OK);
}
#GetMapping(value = "students", params = {"tag"})
public ResponseEntity<?> getByTag(#RequestParam final String tag) {
return new ResponseEntity<>(true, HttpStatus.OK);
}
The above handlers work fine for the following requests:
localhost:8080/test/students?name="Aron"
localhost:8080/test/students?tag="player"
However, whenever I try the following:
localhost:8060/test/students?name="Aron"&tag="player"
it throws java.lang.IllegalStateException: Ambiguous handler methods mapped and responds with an HTTP 500
How can I change this behavior? I want my app to respond only when I get either a tag query parameter or a name query parameter.
For anything else, I want it to ignore even if it's a combination of two parameters.
Why is it throwing the ambiguous error here and how can we handle that?
You can use #RequestParam(required = false):
#GetMapping(value = "students")
public ResponseEntity<?> get(
#RequestParam(required = false) final String name,
#RequestParam(required = false) final String tag) {
if ((name == null) == (tag == null)) {
return new ResponseEntity<>(false, HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<>(true, HttpStatus.OK);
}
it seems you can use negations in params. Something like:
#GetMapping(value = "students", params = {"name", "!tag"})
public ResponseEntity<?> getByName(#RequestParam final String name) {
return new ResponseEntity<>(true, HttpStatus.OK);
}
#GetMapping(value = "students", params = {"tag", "!name"})
public ResponseEntity<?> getByTag(#RequestParam final String tag) {
return new ResponseEntity<>(true, HttpStatus.OK);
}
References: Advanced #RequestMapping options
I'm trying to create a #ControllerAdvice class which currently handle a MethodArgumentNotValidException exception. I've created an exception wrapper for the response which have an errorMessage and statusCode attribute.
public class ExceptionBodyResponse {
private String exceptionMessage;
private int statusCode;
}
And the #ControllerAdvice :
#ControllerAdvice
public class DTOExceptionHandler {
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseBody
public ExceptionBodyResponse handleInvalidArgumentException(MethodArgumentNotValidException exception) {
return GenericBuilder.of(ExceptionBodyResponse::new)
.with(ExceptionBodyResponse::setExceptionMessage, exception.getMessage())
.with(ExceptionBodyResponse::setStatusCode, HttpStatus.BAD_REQUEST.value())
.build();
}
}
And finally, the DTO class with #NotBlank validation:
public class RegisterRequestDto {
#NotBlank
private String email;
#NotBlank(message = "Password must not be null!")
private String password;
}
What I'm expecting as response when I'm sending a JSON with this structure :
{
"email":"stack#yahoo.com";
}
is the following error message:
{
"exceptionMessage":"Password must not be null",
"statusCode":400
}
Instead, I'm getting this:
"exceptionMessage": "Validation failed for argument [0] in public packagehere.UserDto packagehere.UserControllerImpl.save(packagehere.RegisterRequestDto): [Field error in object 'registerRequestDto' on field 'password': rejected value [null]; codes [NotBlank.registerRequestDto.password,NotBlank.password,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [registerRequestDto.password,password]; arguments []; default message [password]]; default message [Password must not be null!]] ",
"statusCode": 400
Try this.
#ControllerAdvice
#Order(Ordered.HIGHEST_PRECEDENCE)
public class DTOExceptionHandler extends ResponseEntityExceptionHandler{
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
final MethodArgumentNotValidException exception,
final HttpHeaders headers,
final HttpStatus status,
final WebRequest request) {
return GenericBuilder.of(ExceptionBodyResponse::new)
.with(ExceptionBodyResponse::setExceptionMessage, exception.getMessage())//You can customize message
.with(ExceptionBodyResponse::setStatusCode, HttpStatus.BAD_REQUEST.value())
.build();
}
}
You can make use of ex.getBindingResult().getFieldErrors() to get the list of FieldError
You need to override method inside #ControllerAdvice :
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
List<String> details = new ArrayList<>();
for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
details.add("{\"" + fieldError.getField() + "\" : \"" + fieldError.getDefaultMessage() + "\"}");
}
return new ResponseEntity<>(details.toString(), BAD_REQUEST);
}
Response is JSON and looks like :
[
{
"employeeLogin": "Pole wymagane"
},
{
"type": "Pole wymagane"
},
{
"description": "Pole wymagane"
},
{
"name": "Pole wymagane"
}]
where validated object is :
#NotBlank(message = "Pole wymagane")
private String employeeLogin;
#NotBlank(message = "Pole wymagane")
private String type;
#NotBlank(message = "Pole wymagane")
private String name;
#NotBlank(message = "Pole wymagane")
private String description;
Described step by step and solved here :
https://howtodoinjava.com/spring-boot2/spring-rest-request-validation/
I do have a class CustomResponseEntityExceptionHandler class extending ResponseEntityExceptionHandler with 2 methods one for handling wrong formats and the other one when a resource (url) is requested but does not exist. I would like to give the user a custom message instead of the default White Label Error page of Spring. I'm trying to understand why my handleResourceNotFoundException method from CustomResponseEntityExceptionHandler is not called when a non existing URI is requested but instead I keep having the White Label Error page. Thanks for your help!
curl localhost:8080/doesnotexist
This is my CustomResponseEntityExceptionHandler
#ExceptionHandler(Exception.class)
public final ResponseEntity<ErrorDetails> handleAllWrongFormatExceptions(WrongFormatException ex,
WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(true));
return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
}
#ExceptionHandler(ResourceNotFoundException.class)
public final ResponseEntity<ErrorDetails> handleResourceNotFoundException(ResourceNotFoundException ex,
WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
Simple class for holding custom error details
public class ErrorDetails {
private Date timestamp;
private String message;
private String details;
public ErrorDetails(Date timestamp, String message, String details) {
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
public Date getTimestamp() {
return timestamp;
}
public String getMessage() {
return message;
}
public String getDetails() {
return details;
} }
And here is my controller
#Controller
public class StringManipController {
#Autowired
private StringProcessingService stringProcessingService;
#GetMapping("/")
#ExceptionHandler(ResourceNotFoundException.class)
#ResponseBody
public String home(#RequestParam(name = "value", required = false, defaultValue = "") String value) {
return "Welcome to the String Processing Application";
}
#GetMapping("/stringDedup")
#ResponseBody
public ProcessedString doManip(#RequestParam(name = "value", required = false, defaultValue = "") String value) {
String result = stringProcessingService.getStringManipulation(value);
return new ProcessedString(result);
}
#GetMapping("/writeNumber")
#ResponseBody
public ProcessedString getWriteNumber(
#RequestParam(name = "value", required = false, defaultValue = "") String value) {
String number = stringProcessingService.getNumberToWords(value);
return new ProcessedString(number);
} }
And here the ResourceNotFoundException class
#ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
private static final long serialVersionUID = 266853955330077478L;
public ResourceNotFoundException(String exception) {
super(exception);
} }
If you used Spring Boot:
#ControllerAdvice
public class CustomResponseEntityExceptionHandler {
#ExceptionHandler(value = { NoHandlerFoundException.class })
public ResponseEntity<Object> noHandlerFoundException(Exception ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("test");
}
}
or
#ControllerAdvice
public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleNoHandlerFoundException(
NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return handleExceptionInternal(ex, "Test", headers, status, request);
}
}
In the org.springframework.web.servlet.DispatcherServlet class there is a variable called throwExceptionIfNoHandlerFound. If this is set to true a method called noHandlerFound will throw a NoHandlerFoundException. Your exception handler will now catch it.
Add this property to your application.properties file: spring.mvc.throw-exception-if-no-handler-found=true