I want to handle MethodArgumentNotValidException in a Springboot application. My entry point is expecting a valid JSON object. I do intercept the error message correctly:
"message": "JSON parse error: Cannot deserialize value of type `java.util.LinkedHashMap<java.lang.String,java.lang.Object>` from Array value (token `JsonToken.START_ARRAY`); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type `java.util.LinkedHashMap<java.lang.String,java.lang.Object>` from Array value (token `JsonToken.START_ARRAY`)\n at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 5, column: 7] (through reference chain: fr.ws.ui.model.request.RequestModel[\"data\"]->java.util.ArrayList[0])",
That's great... Here is my exception handler implementation.
#ExceptionHandler(value = { MethodArgumentNotValidException.class, HttpMessageNotReadableException.class })
public ResponseEntity<ErrorMessage> handleHttpMessageNotReadableException(HttpMessageNotReadableException ex,
WebRequest req) throws JsonProcessingException {
// weird behavior, I got the json but not complete
String output;
try {
output = IOUtils.toString(((ServletWebRequest) req).getRequest().getInputStream(), StandardCharsets.UTF_8);
System.out.println(output);
} catch (IOException e) {
throw new RuntimeException(e);
}
ErrorMessage errorMessage = ErrorMessage.builder()
.operationId(Utils.generateId())
.status(RequestOperationStatus.ERROR.name())
.message(ex.getMessage())
.createdAt(new Date())
.data(output)
.build();
return new ResponseEntity<>(errorMessage, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
}
Now, I want to retrieve the body of my request and I want to parse the content to extract data for building the ErrorMessage body. For this I used org.apache.commons.io library:
IOUtils.toString(((ServletWebRequest) req).getRequest().getInputStream(), StandardCharsets.UTF_8)
The request contains several properties and a file in Base64 format.
It works fine but not totally: I do not retrieve the full request body, I do not get the request body start. My String seems truncated. How can i get the whole request as String ?
The application is Spring Boot 2.6.10 uses WebClient calling third party API, it returns beautiful error message
{
"code": "NOT_FOUND",
"message": "The Reason.",
"correlation_id": "64615a19-f7e5-6c11-2dd3-4bcf92608cfc"
}
My Client class
public User getUserById(String id) {
return client.get().uri("/{id}", id).retrieve()
.onStatus(HttpStatus::isError, clientResponse -> {
throw new ApiException(HttpStatus.BAD_REQUEST, "Error");
}).bodyToMono(User.class).block();
}
My controller
#GetMapping("/users/{id}")
public User getUserById(#PathVariable("id") String id) {
try {
return userClient.getUserById(id);
} catch(ApiException e) {
throw new ResponseStatusException(e.getStatus(), e.getMessage());
}
}
Error message is ugly, including stack trace, my own exception is wrapped in debugInfo
{
"message": "An unexpected error occurred processing this request",
"debugInfo": "org.springframework.web.server.ResponseStatusException: 400 BAD_REQUEST \"Error\"\n\tat com.company.package.rest.controllers.UserController.getUserById(UserController.java:41)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat ...
}
How can I make a beautiful error message, I do not have global error handler, just throw ResponseStatusException in my controller
Trying to send list of Error in response, but in response getting only one parameter of Error type
Public class Error {
private ErrorEnum errorCode;
private String errorDesc;
}
Error response Method
#ResponseStatus(HttpStatus.NOT_FOUND)
#ExceptionHandler(DataNotFoundException.class)
#ResponseBody
public ResponseEntity<Errors> handleResourceNotFoundException(HttpServletRequest httpServletRequest,
DataNotFoundException ex) {
Error error = new Error();
if ("POST_NOT_FOUND".equals(ex.getpostName())) {
error.setErrorCode(Error.ErrorCodeEnum.INVALID_POST_ID);
}
else {
error.setErrorCode(Error.ErrorEnum.BAD_REQUEST);
}
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("refIdentifier", String.valueOf(ex.getpostId()));
error.seterrorDesc(String.valueOf(ex.getpostId()));
Errors errors = new Errors();
errors.addErrorsItem(error);
return new ResponseEntity<>(errors, httpHeaders, HttpStatus.NOT_FOUND);
}
Getting error
"errors": [
{
"errorCode": "INVALID_POST_ID"
}
]
Errors, has list of Error and every Error has two parameters (errorCode and errorDesc) where errorCode is type of Enum in class Error
Any suggestion, why am getting only one parameter(errorCode) of Error class in response ?
How do I propogate an exception thrown in a call to a downstream service to the caller method?
I have a service that calculates something and throws an exception in case of error:
{
"timestamp": "2019-03-12T08:21:05.316+0000",
"status": 500,
"error": "Internal Server Error",
"message": "VAKUUTUSVUOSI.MAKSUEHTO is null or not numeric. Can't be added to due date.",
"path": "/rules/yk32/deducePaymentDueDate"
}
But the calling service displays this exception:
{
"timestamp": "2019-03-12T08:30:22.912+0000",
"status": 500,
"error": "Internal Server Error",
"message": "500 null",
"path": "/calculation/annual/payment"
}
How do I get the caller method also to display the message that the service throws "/rules/yk32/deducePaymentDueDate" instead of "Internal Server Error"?
Calling method:
LocalDate paymentDueDate = ykServiceAdapter.yk32DeducePaymentDueDate(requestDTO);
Calling function in the ykServiceadapter:
public LocalDate yk32DeducePaymentDueDate(Yk32RequestDTO requestDTO) {
ResponseEntity<LocalDate> re;
HttpEntity<?> entity = new HttpEntity<>(requestDTO);
try {
re = getRestTemplate().exchange(
buildServiceUrl(externalServiceConfig, RULE_YK32, DEDUCE_PAYMENT_DUEDATE),
HttpMethod.POST, entity,
new ParameterizedTypeReference<LocalDate>() {
});
return re.getBody();
} catch (HttpClientErrorException ex) {
if (HttpStatus.NOT_FOUND.equals(ex.getStatusCode())) {
return null;
} else {
throw ex;
}
}
}
You're working on two separate contexts, via HTTP.
What that means is the Exception generated by yk32DeducePaymentDueDate is transformed to an HTTP 500 response, which might mean the Exception message is used as response body.
Obviously, being that the original Exception gets lost during the HTTP call, RestTemplate is only able to create an HttpClientErrorException based on the HTTP status code
HttpClientErrorException.BadRequest
HttpClientErrorException.Conflict
HttpClientErrorException.Forbidden
HttpClientErrorException.Gone
HttpClientErrorException.MethodNotAllowed
HttpClientErrorException.NotAcceptable
HttpClientErrorException.NotFound
HttpClientErrorException.TooManyRequests
HttpClientErrorException.Unauthorized
HttpServerErrorException.InternalServerError
HttpServerErrorException.NotImplemented
...
In your case the instantiated Exception is
public static class InternalServerError extends HttpServerErrorException {
InternalServerError(String statusText, HttpHeaders headers, byte[] body, #Nullable Charset charset) {
super(HttpStatus.INTERNAL_SERVER_ERROR, statusText, headers, body, charset);
}
}
Only the Exception message might be recovered, if it has been transmitted in the response body.
You might want to look into a custom ResponseErrorHandler, where you can inspect the full HTTP response and react accordingly.
I have defined a pattern for validating email in my Entity class. In my validation exception handler class, I have added handler for ConstraintViolationException. My application utilize SpringBoot 1.4.5.
Profile.java
#Entity
#EntityListeners(AuditingEntityListener.class)
#Table(name = "profile")
public class Profile extends AuditableEntity {
private static final long serialVersionUID = 8744243251433626827L;
#Column(name = "email", nullable = true, length = 250)
#NotNull
#Pattern(regexp = "^([^ #])+#([^ \\.#]+\\.)+([^ \\.#])+$")
#Size(max = 250)
private String email;
....
}
ValidationExceptionHandler.java
#ControllerAdvice
public class ValidationExceptionHandler extends ResponseEntityExceptionHandler {
private MessageSource messageSource;
#Autowired
public ValidationExceptionHandler(MessageSource messageSource) {
this.messageSource = messageSource;
}
#ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Object> handleConstraintViolation(ConstraintViolationException ex,
WebRequest request) {
List<String> errors = new ArrayList<String>();
....
}
}
When I run my code and pass invalid email address, I get the following exception. The code in handleConstraintViolation is never executed. The http status returned in the exception is 500, but I want to return 400. Any idea how I can achieve that?
2017-07-12 22:15:07.078 ERROR 55627 --- [nio-9000-exec-2] o.h.c.s.u.c.UserProfileController : Validation failed for classes [org.xxxx.common.service.user.domain.Profile] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
ConstraintViolationImpl{interpolatedMessage='must match "^([^ #])+#([^ \.#]+\.)+([^ \.#])+$"', propertyPath=email, rootBeanClass=class org.xxxx.common.service.user.domain.Profile, messageTemplate='{javax.validation.constraints.Pattern.message}'}]
javax.validation.ConstraintViolationException: Validation failed for classes [org.xxxx.common.service.user.domain.Profile] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
ConstraintViolationImpl{interpolatedMessage='must match "^([^ #])+#([^ \.#]+\.)+([^ \.#])+$"', propertyPath=email, rootBeanClass=class org.xxxx.common.service.user.domain.Profile, messageTemplate='{javax.validation.constraints.Pattern.message}'}]
at org.hibernate.cfg.beanvalidation.BeanValidationEventListener.validate(BeanValidationEventListener.java:138)
at org.hibernate.cfg.beanvalidation.BeanValidationEventListener.onPreInsert(BeanValidationEventListener.java:78)
You cannot catch ConstraintViolationException.class because it's not propagated to that layer of your code, it's caught by the lower layers, wrapped and rethrown under another type. So that the exception that hits your web layer is not a ConstraintViolationException.
In my case, it's a TransactionSystemException.
I'm using #Transactional annotations from Spring with the JpaTransactionManager. The EntityManager throws a rollback exception when somethings goes wrong in the transaction, which is converted to a TransactionSystemException by the JpaTransactionManager.
So you could do something like this:
#ExceptionHandler({ TransactionSystemException.class })
public ResponseEntity<RestResponseErrorMessage> handleConstraintViolation(Exception ex, WebRequest request) {
Throwable cause = ((TransactionSystemException) ex).getRootCause();
if (cause instanceof ConstraintViolationException) {
Set<ConstraintViolation<?>> constraintViolations = ((ConstraintViolationException) cause).getConstraintViolations();
// do something here
}
}
Just want to add something. I was trying to do the same thing, validating the entity. Then I realized Spring has already everything out of the box if you validate the controller's input.
#RequestMapping(value = "/profile", method = RequestMethod.POST)
public ProfileDto createProfile(#Valid ProfileDto profile){
...
}
The #Valid annotation will trigger the validation with the javax.validation annotations.
Suppose you have a Pattern annotation on your profile username with a regexp not allowing whitespaces.
Spring will build a response with status 400 (bad request) and a body like this one:
{
"timestamp": 1544453370570,
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"Pattern.ProfileDto.username",
"Pattern.username",
"Pattern.java.lang.String",
"Pattern"
],
"arguments": [
{
"codes": [
"profileDto.username",
"username"
],
"arguments": null,
"defaultMessage": "username",
"code": "username"
},
[],
{
"defaultMessage": "^[A-Za-z0-9_\\-.]+$",
"arguments": null,
"codes": [
"^[A-Za-z0-9_\\-.]+$"
]
}
],
"defaultMessage": "must match \"^[A-Za-z0-9_\\-.]+$\"",
"objectName": "profileDto",
"field": "username",
"rejectedValue": "Wr Ong",
"bindingFailure": false,
"code": "Pattern"
}
],
"message": "Validation failed for object='profileDto'. Error count: 1",
"path": "/profile"
}
Following solution is based on Spring Boot 2.1.2.
To clarify things... as nimai already correctly mentioned:
You cannot catch ConstraintViolationException.class because it's not propagated to that layer of your code, it's caught by the lower layers, wrapped and rethrown under another type. So that the exception that hits your web layer is not a ConstraintViolationException.
In your case it is probably a DataIntegrityViolationException, which points out a problem in the persistence layer. But you don't want to let it come that far.
Solution
Make use of the #Valid annotation for the entity given as method parameter as Ena mentioned. On my version it was missing the org.springframework.web.bind.annotation.RequestBody annotation (Without the #RequestBody annotation the ProfileDto cannot be parsed correctly into your ProfileDto entity and the properties are resulting in null values, e.g. NullPointerException.):
#RequestMapping(value = "/profile", method = RequestMethod.POST)
public ProfileDto createProfile(#Valid #RequestBody ProfileDto profile){
...
}
This will then return your wanted status code 400 and some default response body accompanied by a org.springframework.web.bind.MethodArgumentNotValidException before even reaching the persistence layer. The processing of the MethodArgumentNotValidException is defined in org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler.
This is another topic, but you then have the option to override that behaviour by creating a #ControllerAdvice with #ExceptionHandler(MethodArgumentNotValidException.class) and customize the response body to your needs, since the default error response body is not optimal and not even present when excluding ErrorMvcAutoConfiguration.
Caution: Locating the #ExceptionHandler(MethodArgumentNotValidException.class) inside the #ControllerAdvice that extends the ResponseEntityExceptionHandler results into an IllegalStateException, because in the ResponseEntityExceptionHandler already is an exception handler defined for MethodArgumentNotValidException. So just put it into another #ControllerAdvice class without extending anything.
Alternative manual approach
I saw you can also trigger the validation of the email pattern manually (see Manually call Spring Annotation Validation). I didn't test it myself, but I personally don't like that approach, because it is just bloating your controller code and I currently can't think of a use case that requires it.
I hope that helps others encountering a similar issue.
You cannot catch ConstraintViolationException.class because it's not propagated to that layer of your code, it's caught by the lower layers, wrapped and rethrown under another type. So that the exception that hits your web layer is not a ConstraintViolationException.
So you could do something like this:
#ExceptionHandler({TransactionSystemException.class})
protected ResponseEntity<Object> handlePersistenceException(final Exception ex, final WebRequest request) {
logger.info(ex.getClass().getName());
//
Throwable cause = ((TransactionSystemException) ex).getRootCause();
if (cause instanceof ConstraintViolationException) {
ConstraintViolationException consEx= (ConstraintViolationException) cause;
final List<String> errors = new ArrayList<String>();
for (final ConstraintViolation<?> violation : consEx.getConstraintViolations()) {
errors.add(violation.getPropertyPath() + ": " + violation.getMessage());
}
final ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, consEx.getLocalizedMessage(), errors);
return new ResponseEntity<Object>(apiError, new HttpHeaders(), apiError.getStatus());
}
final ApiError apiError = new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage(), "error occurred");
return new ResponseEntity<Object>(apiError, new HttpHeaders(), apiError.getStatus());
}
I would double check you've imported the right ConstraintViolationException
The one you want is from the org.hibernate.exception.ConstraintViolationException package. If you've imported the javax.validation.ConstraintViolationException it will be skipped as you've experienced.
import org.hibernate.exception.ConstraintViolationException;
#RestController
public class FeatureToggleController {
#ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Object> handleConstraintViolation(ConstraintViolationException ex, WebRequest request) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
}
}
This will be called as expected.
Just check all Exceptions and select the one you need
Need to determine the cause:
while ((cause = resultCause.getCause()) != null && resultCause != cause) {
resultCause = cause;
}
Use instanceof
#ExceptionHandler(Exception.class)
protected ResponseEntity<MyException> handleExceptions(Exception e) {
String message;
Throwable cause, resultCause = e;
while ((cause = resultCause.getCause()) != null && resultCause != cause) {
resultCause = cause;
}
if (resultCause instanceof ConstraintViolationException) {
message = (((ConstraintViolationException) resultCause).getConstraintViolations()).iterator().next().getMessage();
} else {
resultCause.printStackTrace();
message = "Unknown error";
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new MyException(message));
}
That is my solution...
#ExceptionHandler({DataIntegrityViolationException.class})
protected ResponseEntity<Object> handlePersistenceException(final DataIntegrityViolationException ex) {
Throwable cause = ex.getRootCause();
if (cause instanceof SQLIntegrityConstraintViolationException) {
SQLIntegrityConstraintViolationException consEx = (SQLIntegrityConstraintViolationException) cause;
final ApiErrorResponse apiError = ApiErrorResponse.newBuilder()
.message(consEx.getLocalizedMessage())
.status(HttpStatus.BAD_REQUEST)
.build();
return new ResponseEntity<>(apiError, new HttpHeaders(), apiError.getStatus());
}
final ApiErrorResponse apiError = ApiErrorResponse.newBuilder()
.message(ex.getLocalizedMessage())
.status(HttpStatus.NOT_ACCEPTABLE)
.build();
return new ResponseEntity<>(apiError, new HttpHeaders(), apiError.getStatus());
}
#ExceptionHandler(RollbackException.class)
public ResponseEntity<ApiErrorsListResponse> handleNotValidException(RollbackException ex){
String errMessage = ex.getCause().getMessage();
List<String> listErrMessage = getListErrMessage(errMessage);
ApiErrorsListResponse response = ApiErrorsListResponse.newBuilder()
.status(HttpStatus.NOT_ACCEPTABLE)
.errorMessage(listErrMessage)
.build();
return new ResponseEntity<>(response, HttpStatus.NOT_ACCEPTABLE);
}
public static List<String> getListErrMessage(String msg){
Stream<String> stream = Arrays.stream(msg.split("\n"))
.filter(s -> s.contains("\t"))
.map(s -> s.replaceAll("^([^\\{]+)\\{", ""))
.map(s -> s.replaceAll("[\"]", ""))
.map(s -> s.replaceAll("=", ":"))
.map(s -> s.replaceAll("interpolatedMessage", "message"))
.map(s -> s.replaceAll("\\{|\\}(, *)?", ""));
return stream.collect(Collectors.toList());
}
bean
public class ApiErrorsListResponse {
private HttpStatus status;
private List<String> errorMessage;
public ApiErrorsListResponse() {
}
...
}
Try this way..
#ControllerAdvice
public class ControllerAdvisor extends ResponseEntityExceptionHandler {
#Autowired
BaseResponse baseResponse;
#ExceptionHandler(javax.validation.ConstraintViolationException.class)
public ResponseEntity<BaseResponse> inputValidationException(Exception e) {
baseResponse.setMessage("Invalid Input : " + e.getMessage());
return new ResponseEntity<BaseResponse>(baseResponse, HttpStatus.BAD_REQUEST);
}
}
I think you should add #ResponseStatus(HttpStatus.BAD_REQUEST) to your #ExceptionHandler:
#ExceptionHandler(ConstraintViolationException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<Object> handleConstraintViolation(ConstraintViolationException ex, WebRequest request) {
List<String> errors = new ArrayList<String>();
....
}
#ResponseBody
#ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
#ExceptionHandler(DataIntegrityViolationException.class)
public Map errorHandler(DataIntegrityViolationException ex) {
Map map = new HashMap();
map.put("rs_code", 422);
map.put("rs_msg", "data existed !");
return map;
}
just catch org.springframework.dao.DataIntegrityViolationException.
You can handle org.hibernate.exception.ConstraintViolationException by adding this in your #controllerAdvice
#ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity handleConstraintViolationException(Exception ex){
String errorMessage = ex.getMessage();
errorMessage = (null == errorMessage) ? "Internal Server Error" : errorMessage;
List<String> details = new ArrayList<>();
details.add(ex.getLocalizedMessage());
return new ResponseEntity<ErrorResponseDTO>(
new ErrorResponseDTO( errorMessage ,details), HttpStatus.INTERNAL_SERVER_ERROR);
}
You can also catch ConstraintViolationException and throw own exception with #ResponseStatus code or another one and catch it in #ExceptionHandler(YourCustomException.class). If you want to do that you need to implements JpaRepository. During save you should to call saveAndFlush methods that means your code will be execute immediately in DB and you will be able to catch exception i try catch block. If you want, you can do it generic like that:
imports
...
public class ErrorHandler {
public static <T> T execute(Supplier<T> repositorySaveFunction) {
try {
return repositorySaveFunction.get();
} catch (DataIntegrityViolationException e) {
if (e.getCause() instanceof org.hibernate.exception.ConstraintViolationException) {
throw new CustomObjectAlreadyExistException();
}
if (e.getCause() instanceof PropertyValueException) {
var fieldName = ((PropertyValueException) e.getCause()).getPropertyName();
throw new CustomNotNullException(fieldName);
}
throw e;
} catch (javax.validation.ConstraintViolationException e) {
e.getConstraintViolations().forEach(constraintViolation -> {
throw new CustomNotNullException(constraintViolation.getPropertyPath());
});
throw e;
}
}
}
Service:
imports
...
#Service
#Transactional
public class Service {
private final YourRepository yourRepository;
... constructor
public ObjectToSave save(ObjectToSave objectToSave) {
return execute(() -> yourRepository.saveAndFlush(objectToSave));
}
}