I'm currently trying to provide custom messages for exceptions, but ran into an issue with HttpMessageNotReadableException.
I have an ErrorDetails class:
public class ErrorDetails {
private Date timestamp;
private String message;
private String details;
public ErrorDetails(Date timestamp, String message, String details) {
super();
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
public Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getDetails() {
return details;
}
public void setDetails(String details) {
this.details = details;
}
I also have a custom exception handler:
#Order(Ordered.HIGHEST_PRECEDENCE)
#ControllerAdvice
#RestController
public class CustomizedExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(HttpMessageNotReadableException.class)
#Override
public final ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request){
ErrorDetails errorDetails = new ErrorDetails(new Date(), "hello",request.getDescription(true));
errorDetails.setMessage("Testing message");
return new ResponseEntity<>(errorDetails,HttpStatus.NOT_ACCEPTABLE);
}
}
But when i try to post a bad request, for example, with a field that should have a integer value I pass a string in the JSON it still returns the default error message of:
{
"timestamp": "2019-03-12T00:15:14.210+0000",
"status": 400,
"error": "Bad Request",
"message": "JSON parse error: Cannot deserialize value of type `int` from String \"lala\": not a valid Integer value; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `int` from String \"lala\": not a valid Integer value\n at [Source: (PushbackInputStream); line: 5, column: 17] (through reference chain: com.tdl.model.ToDoNote[\"priority\"])",
"path": "/todos"
}
The JSON request:
{
"name": "An workout",
"dateToComplete": "Today",
"description": "Sleep Day",
"priority": "lala",
"completed": false
}
The desired effect would just be the test message appearing instead of the long description.
I also get this in my Eclipse console:
WARN 16508 --- [nio-5000-exec-4] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type int from String "lala": not a valid Integer value; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type int from String "lala": not a valid Integer value
at [Source: (PushbackInputStream); line: 5, column: 17] (through reference chain: com.tdl.model.ToDoNote["priority"])]
I changed the status to NOT_ACCEPTABLE just to see more clearly if my custom error is returned.
Any help would be appreciated. Thank you.
EDIT
Added ExceptionHandler for InvalidFormatException, but nothing changed. I still get the default error(exception) message same as before.
#ExceptionHandler(InvalidFormatException.class)
public final ResponseEntity<Object> handleInvalidFormat(InvalidFormatException ex, HttpHeaders headers, HttpStatus status, WebRequest request){
ErrorDetails errorDetails = new ErrorDetails(new Date(), "hello",request.getDescription(true));
errorDetails.setMessage("Testing message");
return new ResponseEntity<>(errorDetails,HttpStatus.NOT_ACCEPTABLE);
}
I ran into this error HttpMessageNotReadableException and I felt the need of customizing it. After a few trials, I ended up with a better and more readable format.
Step 1: Create a Custom Error Details class with the fields that you would want to expose to the client. Below is what I created.
public class ErrorDetails {
private final Date timestamp;
private final String message;
private final String details;
public ErrorDetails(Date timestamp, String message, String details) {
this.timestamp = timestamp;
this.message = message;
this.details=details;
}
// getters not included for brevity
Step 2: Create a class that will extend the ResponseEntityHandler which has the exceptions that can be overridden. Here, override the handleHttpMessageNotReadbale method, and then in the method have an implementation of your own custom error handler.
#ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(
HttpMessageNotReadableException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(),request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
}
}
Step 3: Run your POST or PUT method with the wrong input fields and check the result. For instance, gender is an enum class with only FEMALE and MALE.
{
"firstName":"Dell",
"lastName":"HP",
"birthYear":"2000-02-12",
"email":"dell#gmail.com",
"gender":"BOY"
}
The response is like below:
{
"timestamp": "2022-06-06T08:08:53.906+00:00",
"message": "JSON parse error: Cannot deserialize value of type com.io.clinic.utils.Gender from String "BOY": not one of the values accepted for Enum class: [FEMALE, MALE]; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type com.io.clinic.utils.Gender from String "BOY": not one of the values accepted for Enum class: [FEMALE, MALE]\n at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 6, column: 14] (through reference chain: com.io.clinic.payloadDTO.PatientDTO["gender"])",
"details": "uri=/api/v1/patients"
}
I was satisfied with having the message in that state for debugging but you can also customize the message response in the overridden method.
The problem is solved. I had my custom exception classes in a badly named package. It was called just exception. While it should have been com.app.exception where the whole project is.
Related
I have an endpoint:
#GetMapping("/v2/tw/{id}")
public TwDto getTw(Authentication auth, #PathVariable Long id) {
}
When I want to fetch data with Postman with wrong parameter type /v2/tw/variableNoNumber, then I want to have some logs in service, I want to get some validation error. How to do that?
Should I add eg. #NumberFormat? It doesn't work.
I use #ControllerAdvice, my project is quite old, everything is validated properly but THIS case is weird for me....
You can write your custom exception handler.
This is your custom entity.
public class ApiException {
private final String message;
private final HttpStatus httpStatus;
public ApiException(String message, HttpStatus httpStatus ) {
this.message = message;
this.httpStatus = httpStatus;
this.timestamp = timestamp;
}
public String getMessage() {
return message;
}
public HttpStatus getHttpStatus() {
return httpStatus;
}
}
This is your exception class.
public class ApiRequestException extends RuntimeException {
public ApiRequestException(String message) {
super(message);
}
public ApiRequestException(String message, Throwable cause) {
super(message, cause);
}
}
And finally ApiExceptionHandler
#ControllerAdvice
public class ApiExceptionHandler {
#ExceptionHandler(value = {ApiRequestException.class})
public ResponseEntity<Object> handleApiRequestException(ApiRequestException e) {
HttpStatus badRequest = HttpStatus.BAD_REQUEST;
ApiException apiException = new ApiException(e.getMessage(), badRequest));
return new ResponseEntity<>(apiException, badRequest);
}
}
For example, if you want to validate the ID you can write as below.
throw new ApiRequestException("Case by id" + TwDto.getId+ " was not found!");
I think I found solution for my problem - instead of throwing 400 with some message maybe it would be sufficient to add:
#GetMapping("/v2/tw/{id:\d+}")
public TwDto getTw(Authentication auth, #PathVariable Long id) {
}
Then I get 404 error Not found and I think this approach is better than returning 400 error without any message... What do you think?
you can use the annotation #Valid for the parameter and handle the exception with the ConstraintViolationException. For more information and detail see here:
https://medium.com/#aamine/customized-input-validation-in-spring-boot-1927aa440bc6
I'm using javax and Hibernate implementation for validation of my request payload.
version - org.hibernate:hibernate-validator:5.4.1.final
Sample Pojo:
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
public class Customer implements Serializable{
#NotNull(message="name is null")
#Size(min = 1)
public String name;
#NotNull(message="country is null")
#Size(min = 1)
public String country;
}
Sample controller:
#RequestMapping(value = {"/customer"}, method = RequestMethod.POST)
#ResponseBody
public Customer doAdd(#RequestBody #Valid Customer inData){
//some logic
return outData
}
Sample input json:
{
"country":"canada"
}
Console Exception:
org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument at index 0 in method: ........ model.request.Customer,java.lang.String,java.lang.String,javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse), with 1 error(s): [Field error in object 'Customer' on field 'name': rejected value [null]; codes [NotNull.Customer.name,NotNull.name,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [Customer.name,name]; arguments []; default message [name]]; default message [name is null]]
Here when customer.name is null, i'm getting response code as 422(Unprocessable entity). But i want to return 400(BAD Request). How can i override the response code here? Any document reference would be appreciated.
note - i don't want to do these validation at controller's side, where can i can check and send the response code accordingly.
Like this one - How to return 400 http status code with #NotNull?
Update - issue was resolved -------
The issue is resolved for me. Thanks all for your response.
Let me explain what was causing this,
HttpRequest ----> #Valid on RequestBody ----> Javax validation on Request object-----> If any of the validation fails, **MethodArgumentNotValidException** exception is thrown.
It is the responsibility of the developer to catch this exception and throw corresponding http response code.
In my case, the exception handler was already there and it was catching this MethodArgumentNotValidException and returning HttpStatus.UNPROCESSABLE_ENTITY. Thats the reason i was seeing 422 error code.
Now i have changed the exception handler as below,
#ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResource>
handleMethodArgumentNotValidException(MethodArgumentNotValidException exception, HttpServletRequest request) {
List<FieldErrorResource> fieldErrorResources = new ArrayList<>();
BindingResult bindingResult = exception.getBindingResult();
for (FieldError constraintViolation : bindingResult.getFieldErrors()) {
fieldErrorResources.add(FieldErrorResource.builder()
.field(constraintViolation.getField())
.resource(request.getContextPath())
.message(constraintViolation.getDefaultMessage()).build());
}
return responseEntityFor(HttpStatus.BAD_REQUEST,
"The content you've sent contains " + bindingResult.getErrorCount() + " validation errors.", fieldErrorResources);
}
You should set BindingResult immediately after your Customer. Like:
#RequestMapping(value = {"/customer"}, method = RequestMethod.POST)
#ResponseBody
public Customer doAdd(#RequestBody #Valid Customer inData, BindingResult bindingResult){
//some logic
return outData
}
You can use RestControllerAdvice for the same
#RestControllerAdvice
public class ExceptionRestControllerAdvice {
#ExceptionHandler({ConstraintViolationException.class})
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
public ExceptionResponseMessage handleInvalidParameterException(RuntimeException ex) {
return sendResponse(HttpStatus.BAD_REQUEST, ex);
}
private ExceptionResponseMessage sendResponse(HttpStatus status, RuntimeException ex) {
return new ExceptionResponseMessage(Instant.now(), status.value(), status.getReasonPhrase(),
ex.getClass().toString(), ex.getMessage());
}
}
public class ExceptionResponseMessage {
private Instant time;
private int status;
private String error;
private String exception;
private String message;
// setter and getter and constructor
I try to make user registration codes with spring boot web starter.
First, these are registration form class including constraints.
#Data
public class RegisterForm {
#NotBlank(message = "Not valid username.") // (1)
#Size(min=2, max=30, message="minimum 2 and maximum 30") // (3)
private String username;
#NotBlank(message = "Not valid password") // (1)
#Size(min=5, message = "minimum 5") // (3)
private String password;
#Size(max=50, message="maximum 50") // (3)
private String fullname;
#NotEmpty(message = "Not valid email") // (2)
private String email;
}
And next codes are controller classes which bind User class and registration form class.
#RequestMapping(value="/users/register", method=RequestMethod.POST)
public String register(#Valid RegisterForm registerForm, Model model, BindingResult bindingResult) {
if(!bindingResult.hasErrors()) {
User user = new User();
user.setUsername(registerForm.getUsername());
user.setPassword(registerForm.getPassword());
user.setEmail(registerForm.getEmail());
user.setFullname(registerForm.getFullname());
user.setRole(UserRole.USER);
this.userService.register(user);
return "redirect:/home";
}
return "/users/register";
}
And Below codes are Error Controller class.
#RequestMapping("/error")
public String errorHandle(HttpServletRequest request) {
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if(status != null) {
Integer statusCode = Integer.valueOf(status.toString());
if(statusCode.equals(HttpStatus.BAD_REQUEST.value())) {
return "/errors/400";
} else if(statusCode.equals(HttpStatus.NOT_FOUND.value())) {
return "/errors/404";
} else if(statusCode.equals(HttpStatus.FORBIDDEN.value())) {
return "/errors/403";
} else if(statusCode.equals(HttpStatus.INTERNAL_SERVER_ERROR.value())) {
return "/errors/500";
}
}
return "errors/default";
}
And I make the error intentionally ,and then the error message are brought on the console like below and 400 exception is thrown with /error/400 html.
Field error in object 'registerForm' on field 'username': rejected value []; default message [minimum 2 and maximum 30]
Field error in object 'registerForm' on field 'username': rejected value []; default message [Not valid username]
Field error in object 'registerForm' on field 'email': rejected value []; default message [Not valid email]
Field error in object 'registerForm' on field 'password': rejected value []; default message [Not valid password]
Field error in object 'registerForm' on field 'password': rejected value [];default message [minimum 5]]
My issue is I have no idea how to send the field error of registerForm messages to /error/400 html so the user can confirm which field of registerForm violates the constraint. I want to know how field error of registerForm can be transferred to /error/400 html. Any idea, please.
first step:
validate data in the controller like this
#RequestMapping(value="/users/register", method=RequestMethod.POST)
public String register(#Valid RegisterForm registerForm)......
sencond step:
make a controller advice which catch the exceptions
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(value = MethodArgumentNotValidException.class)
#ResponseBody
public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
//here you can use api of MethodArgumentNotValidException to do anything you want
//e.getBindingResult(),e.getFieldErrors(),etc;
// you can change the return type of Object
}
You need to extends ResponseEntityExceptionHandler which provide centralized exception handling across all RequestMapping methods. This base class provides an ExceptionHandler method for handling internal Spring MVC exceptions. This method returns a ResponseEntity for writing to the response with a HttpMessageConverter message converter.
#ControllerAdvice
#Order(Ordered.HIGHEST_PRECEDENCE)
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
// All Exceptions Handler.
#ExceptionHandler(Exception.class)
public final ResponseEntity<ExceptionBean> handleAllExceptions(Exception ex, WebRequest request) {...}
// Unique Constraint Violation Exception Handler.
#ExceptionHandler(DataIntegrityViolationException.class)
public final ResponseEntity<ExceptionBean> handleDataIntegrityViolationExceptions(DataIntegrityViolationException ex, WebRequest request) {...}
// Custom Exceptions Handler.
#ExceptionHandler(DomainException.class)
public final ResponseEntity<ExceptionBean> handleDomainExceptions(DomainException ex, WebRequest request) {
ExceptionBean exceptionBean = new ExceptionBean(new Date(), ex.getMessage(),
request.getDescription(false));
LOGGER.error(ex.getMessage(), ex);
return new ResponseEntity<>(exceptionBean, HttpStatus.BAD_REQUEST);
}
}
I am using spring data JPA for creating application. In that I am trying to implement server side validation using annotation. I added #NotNull annotation on filed with custom message. I also added #valid with #RequestBody
But problem is that when I am passing nAccountId as null I am not getting custom message i.e. Account id can not be null I am getting "message": "Validation failed for object='accountMaintenanceSave'. Error count: 1",.
Can any one please tell me why I am not getting custom message?
Controller code
#PutMapping("/updateAccountData")
public ResponseEntity<Object> saveData(#Valid #RequestBody AccountMaintenanceSave saveObj){
return accService.saveData(saveObj);
}
AccountMaintenanceSave class
import javax.validation.constraints.NotNull;
public class AccountMaintenanceSave {
#NotNull(message="Account id can not be null")
public Integer nAccountId;
#NotNull
public String sClientAcctId;
#NotNull
public String sAcctDesc;
#NotNull
public String sLocation;
#NotNull
public Integer nDeptId;
#NotNull
public Integer nAccountCPCMappingid;
#NotNull
public Integer nInvestigatorId;
//Getter and Setter
}
RestExceptionHandler class
#ControllerAdvice
public class RestExceptionHandler {
#ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleAllExceptionMethod(Exception ex, WebRequest requset) {
ExceptionMessage exceptionMessageObj = new ExceptionMessage();
exceptionMessageObj.setMessage(ex.getLocalizedMessage());
exceptionMessageObj.setError(ex.getClass().getCanonicalName());
exceptionMessageObj.setPath(((ServletWebRequest) requset).getRequest().getServletPath());
// return exceptionMessageObj;
return new ResponseEntity<>(exceptionMessageObj, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
I don't know what exactly happen previously and not getting proper message. Now using same code getting result like this with proper message
{
"message": "Validation failed for argument at index 0 in method: public org.springframework.http.ResponseEntity<java.lang.Object> com.spacestudy.controller.AccountController.saveData(com.spacestudy.model.AccountMaintenanceSave), with 1 error(s): [Field error in object 'accountMaintenanceSave' on field 'nAccountId': rejected value [null]; codes [NotNull.accountMaintenanceSave.nAccountId,NotNull.nAccountId,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [accountMaintenanceSave.nAccountId,nAccountId]; arguments []; default message [nAccountId]]; default message [Account id can not be null]] ",
"error": "org.springframework.web.bind.MethodArgumentNotValidException",
"path": "/spacestudy/rockefeller/admin/account/updateAccountData"
}
In message filed can I print only [Account id can not be null]?
Try this.
#ControllerAdvice
public class RestExceptionHandler {
#ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleAllExceptionMethod(Exception ex, WebRequest requset) {
ExceptionMessage exceptionMessageObj = new ExceptionMessage();
// Handle All Field Validation Errors
if(ex instanceof MethodArgumentNotValidException) {
StringBuilder sb = new StringBuilder();
List<FieldError> fieldErrors = ((MethodArgumentNotValidException) ex).getBindingResult().getFieldErrors();
for(FieldError fieldError: fieldErrors){
sb.append(fieldError.getDefaultMessage());
sb.append(";");
}
exceptionMessageObj.setMessage(sb.toString());
}else{
exceptionMessageObj.setMessage(ex.getLocalizedMessage());
}
exceptionMessageObj.setError(ex.getClass().getCanonicalName());
exceptionMessageObj.setPath(((ServletWebRequest) requset).getRequest().getServletPath());
// return exceptionMessageObj;
return new ResponseEntity<>(exceptionMessageObj, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
It's not so good to make your only ExceptionHandler to catch Exception.class make it ConstraintViolationException.class
Another approach to the solution:
I would suggest remove the exception handler for this validation fields in the POJO and rather let Spring handle the error response by adding the below property into application.properties. By adding this the message configured in the #notnull or any validation annotation can be captured and showed in the response by default and no explicit handling of this validation case is required.
server.error.include-message=always
server.error.include-binding-errors=always
I use Jersey API for my REST service. My question is: Is there a more elegant way of returning exceptions in a JSON form? Is it better to concern myself with creating a json object myself and attaching it to the response directly?
This is a simplified example of one of the methods in the service. As you see, I use HashMap only because the method may throw an exception, in which case I need to return information about It.
#Path("/admin")
public class AdminService {
#Context
HttpServletRequest request;
#POST
#Produces(MediaType.APPLICATION_JSON)
public Map<Integer, String> createCompany(Company company){
Map<Integer, String> map = new HashMap<>();
try{
AdminFacade adminFacade = (AdminFacade)Utility.getFacade(request);
adminFacade.createCompany(company);
map.put(1,"success");
} catch (ExceptionREST e) {
map.put(e.getErrorNumber(), e.getMessage());
} finally {
return map;
}
}
}
You can create a class like the one below to represent an error,
#JsonPropertyOrder({ "code", "field", "message" })
public class ErrorInfo {
private String code;
private String field;
private String message;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getField() {
return field;
}
public void setField(String field) {
this.field = field;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
You can create a class which extends an exception like this,
public class InvalidInputException extends RuntimeException {
private static final long serialVersionUID = -5027121014723838738L;
private List<ErrorInfo> errors;
public List<ErrorInfo> getErrors() {
return this.errors;
}
public InvalidInputException(List<ErrorInfo> errors) {
super();
this.errors = errors;
}
public InvalidInputException(String message, List<ErrorInfo> errors) {
super(message);
this.errors = errors;
}
}
And have a exception mapper, where you can convert the List to json and return to the user with http status code 400 (Bad Request).
#Provider
public class InvalidInputExceptionMapper implements ExceptionMapper<InvalidInputException> {
#Override
#Produces(MediaType.APPLICATION_JSON)
public Response toResponse(InvalidInputException e) {
ResponseBuilder rb = Response.status(Status.BAD_REQUEST);
rb.entity(e.getErrors());
return rb.build();
}
}
Http Response will be,
HTTP/1.1 400 BAD REQUEST
{
"errors": [{
"error": {
"code": "100",
"field": null,
"message": "Name is required"
},
"error": {
"code": "100",
"field": null,
"message": "Age is required"
}
}]
}
I believe it is quite popular that people use http response status code to handle the error. E.g. 404 status is not found 5xx is server internal error e.t.c.
You can easily set the error code by using the Response object.
Instead of returning a map, return a Response object.
#Path("/admin")public class AdminService {
#Context
HttpServletRequest request;
#POST
#Produces(MediaType.APPLICATION_JSON)
public Response createCompany(Company company){
Map<Integer, String> map = new HashMap<>();
try{
AdminFacade adminFacade = (AdminFacade)Utility.getFacade(request);
Company commpany=adminFacade.createCompany(company);//this entity annotated by XmlRootElement
Response response=Response.ok().entity(company).build();
} catch (ExceptionREST e) {
response=Response.status(404).build();
} return response;
}}
To make the Restful api more robust, some will return an OK response to prevent "smart redirect" from the server and output some weird html.
you can refer here for a list of http status code and what it mean.
For Java EE Response class, you can refer the official javadoc
You can wrap your error into a class, say I have an ErrorData class which has status, message and stacktrace. Everytime an exception occurs, I throw a GeneralAppException with the errordata object.
public class GeneralAppException extends WebApplicationException {
public GeneralAppException(ErrorData er) {
super(Response.status(er.getStatusCode()).
entity(er).type(MediaType.APPLICATION_JSON).build());
}
}
I have another class which has all the known errors, eg.
public static final ErrorData NODATAFOUND = new ErrorData(Response.Status.NOT_FOUND.getStatusCode(),"No data was found for given query");
public static final ErrorData CODEERROR = new ErrorData(502,"CodeError");
Your catch can look like
catch (ExceptionREST e) {
throw new GeneralAppException(ErrorData.NODATAFOUND);
}
Reference used : https://jersey.java.net/documentation/latest/representations.html#d0e6665