In an REST API generated with JHipster, I want to throw some 404 exceptions. It is normally done with
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
which actualy results in a 404 response to the xhr request. The problem is that in the front side, JHipster parses the response with
angular.fromJson(result)
and such result is empty when the 404 is the actual response, which makes the parse to fail.
If I point to an unmapped URI, lets say /api/user while my controller maps to /api/users (note the plural) the 404 I got from the API has a body in it:
{
"timestamp": "2016-04-25T18:33:19.947+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/api/user/myuser/contact"
}
which is correctly parse in angular.
How can I create a body like this? Is this exception thrown by spring or is tomcat who throws it?
I tried this: Trigger 404 in Spring-MVC controller? but I cant set the parameters of the response.
Basic Idea
First option is to define error objects and return them as 404 Not Found body. Something like following:
Map<String, String> errors = ....;
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errors);
Instead of returning a typical ResponseEntity, you can throw an Exception that will be resolved to a 404 Not Found. Suppose you have a NotFoundException like:
#ResponseStatus(code = HttpStatus.NOT_FOUND)
public class NotFoundException extends RuntimeException {}
Then if you throw this exception in your controllers, you would see something like:
{
"timestamp":1461621047967,
"status":404,
"error":"Not Found",
"exception":"NotFoundException",
"message":"No message available",
"path":"/greet"
}
If you want to customize the message and other parts of body, you should define a ExceptionHandler for NotFoundException.
Introducing Exception Hierarchies
If you're creating a RESTful API and want to have different Error Codes and Error Messages for different exceptional cases, you can create a hierarchy of exceptions representing those cases and extract message and code from each one.
For example, you can introduce an exception, say, APIException which is super-class of all other exceptions thrown by your controllers. This class defines a code/message pair like:
public class APIException extends RuntimeException {
private final int code;
private final String message;
APIException(int code, String message) {
this.code = code;
this.message = message;
}
public int code() {
return code;
}
public String message() {
return message;
}
}
Each subclass depending on the nature of its exception can provide some sensible values for this pair. For example, we could have an InvalidStateException:
#ResponseStatus(code = HttpStatus.BAD_REQUEST)
public class InvalidStateException extends APIException {
public InvalidStateException() {
super(1, "Application is in invalid state");
}
}
Or that notorious not found ones:
#ResponseStatus(code = HttpStatus.NOT_FOUND)
public class SomethingNotFoundException extends APIException {
public SomethingNotFoundException() {
super(2, "Couldn't find something!");
}
}
Then we should define an ErrorController that catches those exceptions and turn them to meaningful JSON representations. That error controller may look like following:
#RestController
public class APIExceptionHandler extends AbstractErrorController {
private static final String ERROR_PATH = "/error";
private final ErrorAttributes errorAttributes;
#Autowired
public APIExceptionHandler(ErrorAttributes errorAttributes) {
super(errorAttributes);
this.errorAttributes = errorAttributes;
}
#RequestMapping(path = ERROR_PATH)
public ResponseEntity<?> handleError(HttpServletRequest request) {
HttpStatus status = getStatus(request);
Map<String, Object> errors = getErrorAttributes(request, false);
getApiException(request).ifPresent(apiError -> {
errors.put("message" , apiError.message());
errors.put("code", apiError.code());
});
// If you don't want to expose exception!
errors.remove("exception");
return ResponseEntity.status(status).body(errors);
}
#Override
public String getErrorPath() {
return ERROR_PATH;
}
private Optional<APIException> getApiException(HttpServletRequest request) {
RequestAttributes attributes = new ServletRequestAttributes(request);
Throwable throwable = errorAttributes.getError(attributes);
if (throwable instanceof APIException) {
APIException exception = (APIException) throwable;
return Optional.of(exception);
}
return Optional.empty();
}
}
So, if you throw an SomethingNotFoundException, the returned JSON would be like:
{
"timestamp":1461621047967,
"status":404,
"error":"Not Found",
"message":"Couldn't find something!",
"code": 2,
"path":"/greet"
}
I guess you can do this if you want to return some message or test with your error code
#ResponseBody
public ResponseEntity somthing() {
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/json; charset=utf-8");
return new ResponseEntity<>(new Gson().toJson("hello this is my message"), headers, HttpStatus.NOT_FOUND);
}
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "message");
Related
Consider a scenario where I want to create a record in database with the already existing ID. By doing this, we get "500-internal server error". But I want to customize the message returned to "Id already exist, Cannot create record". Below is the my sample existing code:
Controller class :
#PostMapping(value = "/plans")
public ResponseEntity<ResponseSave> savePostPlanDetails(
#Valid #RequestBody(required = true) Plan plan) throws ParseException {
String planId = plan.getPlanId();
Integer createdId = planDataService.savePlan(plan, planId);
ServiceMessage serviceMessage = ServiceMessage.createCreatedServiceMessage();
return new ResponseEntity<>(ResponseSave.createResponseSave(serviceMessage, createdId), HttpStatus.CREATED);
}
Service class :
public Integer savePlan(Plan plan, String planId) throws ParseException {
PlanDao findResponse = planDataRepository.findByPlanId(planId);
if (findResponse != null) {
//This line generate 500 error if create with already existing ID.
throw new IllegalArgumentException(PlanSearchEnum.RECORD_ALREADY_EXIST.getValue());
}
PlanDao planDao1 = requestToResponseMapper.panToPlanDao(plan);
PlanDao saveResponse = planDataRepository.save(planDao1);
return saveResponse.getInternalId();
}
Postman Output :
{
"message": {
"code": "500",
"description": "Unable to process request",
"type": "Internal Server Error"
}
}
As in the above postman response, I want the description to be like : "description": "Id already exist, Cannot create record" instead of the general message as show above. So how to do this ?
You will need to have a handler for the exception:
#ControllerAdvice
#Slf4j
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(value = {IllegalArgumentException.class})
public ResponseEntity<Object> handleIllegalArgumentExceptions(Exception exception, WebRequest webRequest) {
HttpStatus errorCode = HttpStatus.INTERNAL_SERVER_ERROR;
return this.handleExceptionInternal(exception, new ErrorInfo(errorCode.value(), "Id already exist, Cannot create record"), new HttpHeaders(), errorCode, webRequest);
}
}
And the model ErrorInfo:
public class ErrorInfo {
private final int code;
private final String description;
}
Finally, you should definitely consider creating your own exception instead of using the generic IllegalArgumentException. You can create something more meaningful for your business case, such as RecordAlreadyExistsException.
What i trying to achieve is to get my response error with 404 code and the error body with WebClient, how do i do this properly?
here is my response with error code 404 and the body response from another API :
{
"timestamp": "2020-09-02T07:36:01.960+00:00",
"message": "Data not found!",
"details": "uri=/api/partnershipment/view"
}
and here is how my consuming code looked like :
Map<String,Long> req = new HashMap<String,Long>();
req.put("id", 2L);
PartnerShipmentDto test = webClient.post()
.uri(urlTest).body(Mono.just(req), PartnerShipmentDto.class)
.exchange()
.flatMap(res -> {
if(res.statusCode().isError()){
res.body((clientHttpResponse, context) -> {
throw new ResourceNotFound(clientHttpResponse.getBody().toString());
});
throw new ResourceNotFound("aaaa");
} else {
return res.bodyToMono(PartnerShipmentDto.class);
}
})
.block();
and here is my ResourNotFound.java class :
#SuppressWarnings("serial")
#ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFound extends RuntimeException {
public ResourceNotFound(String message){
super(message);
}
}
and here is my Global Exception handler using #ControllerAdvice :
#ControllerAdvice
#RestController
public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public final ResponseEntity<Object> handleAllException(Exception ex, WebRequest request) {
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(), request.getDescription(false));
logger.error(ex.getMessage());
return new ResponseEntity(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
#ExceptionHandler(ResourceNotFound.class)
public final ResponseEntity<Object> handleResourceNotFoundException(ResourceNotFound ex, WebRequest request) {
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(), request.getDescription(false));
logger.error(ex.getMessage());
return new ResponseEntity(exceptionResponse, HttpStatus.NOT_FOUND);
}
}
but the response i got printed in my ResourceNotFound exception is like this (this is my error from consumer side) :
{
"timestamp": "2020-09-02T07:50:48.132+00:00",
"message": "FluxMap",
"details": "uri=/api/shipmentaddressgrouping/store"
}
it written "FluxMap" only, how i get the "message" field? i would like to get the "timestamp" and "details" field too
The main issue with the example code you have give is the following line of code
throw new ResourceNotFound(clientHttpResponse.getBody().toString());
The type of this is Flux<DataBuffer>, not the actual response body. This is leading to the issue you are seeing.
The way to solve this is invoking the bodyToMono method on the error response body and mapping to a java object. This can be done via the onStatus operator expose from the web client that allows you to take specific actions on specific status codes.
The code snippet below should resolve this
webClient.post()
.uri(uriTest).body(Mono.just(req), PartnerShipmentDto.class)
.retrieve()
.onStatus(HttpStatus::isError, res -> res.bodyToMono(ErrorBody.class)
.onErrorResume(e -> Mono.error(new ResourceNotFound("aaaa")))
.flatMap(errorBody -> Mono.error(new ResourceNotFound(errorBody.getMessage())))
)
.bodyToMono(PartnerShipmentDto.class)
.block();
The class ErrorBody should contain all of the fields you want to map from json to the java object. The example below only maps the "message" field.
public class ErrorBody {
private String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
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));
}
}
I am hosting Spring Boot app on weblogic 10.3.6 for REST api
I'd like to implement these 2 features:
Whenever my custom exception occurs I would like to send a http response with message e.g. 500 - "request couldn't be parsed because of something..."
Whenever any error is thrown I would like to get the stack trace that is usually printed to console (for debugging purposes)
I tried to solve the first part the following way:
#ControllerAdvice
public class ExceptionHandlerAdvice {
#ExceptionHandler(MyException.class)
public ResponseEntity handleException(MyException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
}
However as a response I only get 500 Internal Server Error with no message
As for the second part I tried simmilar approach but there was no error message either.
How can I solve this?
EDIT:
The best I could achieve was removing ExceptionHandlerAdvice and using annotation on my exception class instead like this:
#ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR, reason="This is a message I'd like to send")
It works, but I have to add some variables from code to the message and this method does not allow it
EDIT2:
This is a bit weird, perhaps a behavior of weblogic server, but when I set the httpStatus to HttpStatus.ACCEPTED I can see the message, if it is HttpStatus.Forbidden or any other 4xx error I just get the error without message
Create 'ResponseEntity' object with message and status and return it, it will display with error message.
/**
* HTTP_STATUS - 500 -Service Unavailable.
*
* #param exception
* Catches the following: MyException
* #return
*/
#ExceptionHandler({ MyException.class})
#ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
#ResponseBody
public static ResponseEntity<?> handleConnectionErrorResponse(MyException exception) {
return new ResponseEntity<String>("Some error occurred at server", HttpStatus.INTERNAL_SERVER_ERROR);
}
Do not return something, throw an :
#ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY)
public class UnprocessableEntity extends RuntimeException {
public UnprocessableEntity(String string) {
super(string);
}
}
Or another thing like that.
I also went through the same requirement.
Below is the code which is working for me:
String errMsg = "{\"errorMessage\":\"Parking Slot is not available\"}";
return new ResponseEntity<String>(errMsg, HttpStatus.INTERNAL_SERVER_ERROR);
Whereas errMsg should be written in the format which you want. Like I had requirment for response in JSON.
Hope this will help some of you
Well In my case I have done custom error handling logic.
We can define a custom Base Response class wich accepts generic type(Eg: user desired model)
Return BaseResponse as a response for each REST Methods
(GET, PUT, POST, DELETE, etc)
BaseResponse.java
public class BaseResponse<T> {
int statusCode;
String message;
T data;
public int getStatusCode() {
return statusCode;
}
public BaseResponse<T> getValidResponse(String message, T data) {
BaseResponse<T> baseResponse = new BaseResponse<T>();
baseResponse.statusCode = 200;
baseResponse.message = message;
baseResponse.data = data;
return baseResponse;
}
public BaseResponse<T> getErrorResponse(int StatusCode, String message) {
BaseResponse<T> baseResponse = new BaseResponse<T>();
baseResponse.statusCode = StatusCode;
baseResponse.message = message;
return baseResponse;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
User.java
public class User {
String userName;
String userAddress;
String userEmail;
String userPhoneNumber;
...
//Getters & Setters
}
YourController.java
....
#PostMapping("/addUser")
public BaseResponse<User> addUser(User user) {
if (user.getUserName() != null && !user.getUserName().equals("")) {
UserEntity userEntity = new UserEntity();
userEntity.setName(user.getUserName());
...
userRepository.save(userEntity);
return new BaseResponse<User>().getValidResponse("Successfully Added User", user);
} else {
return new BaseResponse<User>().getErrorResponse(400, "Name field is required");
}
}
...
#DeleteMapping("/deleteUser/{userId}")
//Using ? for uncertain Response.Eg: Some response might have data response and some may not have data response...
public BaseResponse<?> deleteUser(#PathVariable(value = "userId") int userId) {
//After delete operation...we don't require data response.
return new BaseResponse<User>().getValidResponse("Successfully deleted the User", null);
}
This might not be an exact solution for the question asked but will surely help someone else.
Considering that we are using Kotlin, Spring Boot, annotations and other related libraries.
If we have a situation in which our code throws an exception, how could we automatically retrieve the method parameters values in the moment of that exception?
Can we do this using AOP, Spring Interceptors or other techniques?
We would like to have this to enrich our error messages so we could replicate the errors from where they occurred.
Please note that we are searching for a solution that we don't need to annotate all possible methods but something that would handle the code when an exception occurs. We can use the Java stacktrace elements to retrieve some useful information like the method, line and file where the exception occurred but we don't have the parameters values there.
In Spring we have the Controller Advice feature that we can use to handle all of our exceptions, so we would like to put something there for this purpose, for example.
Edit
Adding some example code:
fun exceptionHandler(throwable: Throwable) {
logger.severe("""
Error ${throwable.message}
File: ${throwable.stackTrace[2].fileName}
Class: ${throwable.stackTrace[2].className}
Method: ${throwable.stackTrace[2].methodName}
Line: ${throwable.stackTrace[2].lineNumber}
Parameters: ## Somehow get the parameters values here, in this case "Hello, 1, false"
""".trimIndent())
}
fun myController() {
myMethodWithErrors("Hello", 1, false)
}
fun myMethodWithErrors(param1: String, param2: Int, param3: Boolean) {
throw RuntimeException("Some bad thing happened here when executing this code.")
}
I assume that you were talking about rest API parameters and not every single java method parameter. You can implement controller advice that captures all exceptions in your rest API calls.
#ControllerAdvice
public class ExceptionHandler {
#ExceptionHandler(value = [Exception::class])
#ResponseBody
fun onException(exception: Exception, request: WebRequest): ResponseEntity<ErrorDetailsClass> {
log.error("error when request with parameters ${request.parameterMap} ")
return buildDetails(request)
}
}
In this way, you can do both retrieve a proper error message and also log something internally for error tracking purposes.
With Spring AOP this requirement can be met with #AfterThrowing advice.
Following example Aspect will intercept all method calls under package org.aop.bean.impl that exits with an exception . We can further filter to the specific exception type with throwing attribute. The given example filters out the methods exiting with IllegalArgumentException.
The arguments during the method call can be obtained with joinpoint.getArgs() method.
#Aspect
#Component
public class ExceptionLoggerAspect {
#Pointcut("execution(* org.aop.bean.impl..*(..))")
public void allExceptions() {
}
#AfterThrowing(pointcut = "allExceptions()",throwing="ex")
public void logException(JoinPoint jp , IllegalArgumentException ex) {
Object[] args= jp.getArgs();
for(Object obj:args) {
System.out.println(obj);
}
}
}
From the docs
Often, you want the advice to run only when exceptions of a given type
are thrown, and you also often need access to the thrown exception in
the advice body. You can use the throwing attribute to both restrict
matching (if desired — use Throwable as the exception type otherwise)
and bind the thrown exception to an advice parameter
The example I'm writing is in spring-boot using org.springframework.web.bind.annotation.ExceptionHandler annotation
It works perfectly fine for me
Suppose I made a Get request to https://example.com/user-api/users/a535c777-c906-45e2-b1c3-940965a507f2q , then our api validates if that user-id exists or not and if not throws a proper message including which parameters are invalid or has errors.
Response ex 1:
{
"apierror": {
"dateTime": "2020-02-13T06:24:14.985",
"timestamp": "1581603854985",
"status": 404,
"error": "Not Found",
"message": "User not found",
"debugMessage": null,
"errors": [
{
"field": "userId",
"rejectedValue": "a535c777-c906-45e2-b1c3-940965a507f2q",
"message": "User not found with userId:a535c777-c906-45e2-b1c3-940965a507f2q"
}
]
}
}
Response ex2:
{
"apierror": {
"dateTime": "2020-02-13T06:43:23.377",
"timestamp": "1581605003377",
"status": 400,
"error": "Bad Request",
"message": "Validation error",
"debugMessage": null,
"errors": [
{
"field": "userName",
"rejectedValue": "Ash",
"message": "Username should have at least 6 characters"
},
{
"field": "userName",
"rejectedValue": "Ash",
"message": "Invalid username"
},
{
"field": "password",
"rejectedValue": "shutosh#",
"message": "Invalid password"
}
]
}
}
Exception message "User not found with userId:a535c777-c906-45e2-b1c3-940965a507f2q" is as per the api.
Below is the use-case.
Controller:
#PrivilegeMapper.HasPlaceUserPrivilege
#GetMapping(value = "/{userId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<?> getUserProfile(#NotBlank #PathVariable String userId) {
return myService.buildUserProfile(userId);
}
Service:
#Override
public ResponseEntity<?> buildUserProfile(final String userId) {
ApiUser apiUser = userRepository.findById(userId)
.orElseThrow(() -> new ApiUserNotFoundException("userId",userId));
return ResponseEntity.ok(sirfUser);
}
Exception Classes:
#Getter
#Setter
#ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ApiUserNotFoundException extends NotFoundException {
public ApiUserNotFoundException(String msg, Throwable t) {
super(msg, t);
}
public ApiUserNotFoundException(String msg) {
super(msg);
}
public ApiUserNotFoundException(String key, String value) {
super(key, value);
}
public ApiUserNotFoundException(String key, String value, List<Error> errors) {
super(key, value, errors);
}
}
#Getter
#Setter
#ResponseStatus(code = HttpStatus.NOT_FOUND)
public class NotFoundException extends RuntimeException {
private String key;
private String value;
private List<Error> errors;
public NotFoundException(String msg, Throwable t) {
super(msg, t);
}
public NotFoundException(String msg) {
super(msg);
}
public NotFoundException(String key, String value) {
this.key = key;
this.value = value;
}
public NotFoundException(String key, String value, List<Error> errors) {
this.key = key;
this.value = value;
this.errors = errors;
}
}
Exception Handler:
#ExceptionHandler(ApiUserNotFoundException.class)
protected ResponseEntity<Object> handleSirfUserNotFound(ApiUserNotFoundException ex) {
log.error(String.format("User not found with %s:%s",ex.getKey(),ex.getValue()));
ApiError apiError = new ApiError(NOT_FOUND);
apiError.setMessage("User not found");
List<Error> errors = new ArrayList<>();
Error error = new ApiValidationError(SirfUser.class.getSimpleName());
((ApiValidationError) error).setMessage(String.format("User not found with %s:%s",ex.getKey(),ex.getValue()));
((ApiValidationError) error).setField(ex.getKey());
((ApiValidationError) error).setRejectedValue(ex.getValue());
errors.add(error);
apiError.setErrors(errors);
return buildResponseEntity(apiError);
}
And this is it. You are done. such type of handling is always useful both for logging and ui perspective.