How to get custom error body message in WebClient properly? - java

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;
}
}

Related

Return list of Errors using ResponseEntity

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 to customize the internal server error message (error code : 500)

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.

How to get json response in Java in case of 4XX and 5XX error

I was trying out RestTemplate and Retrofit2. Both the libraries throw exception in case api returns 4XX/5XX. The api when hit from postman gives a JSON response body, along with 4XX/5XX.
How can I retrieve this JSON response using RestTemplate or Retrofit2.
Thanks.
Use the HttpClientErrorException, HttpStatusCodeException after try block as below.
try{
restTemplate.exchange("url", HttpMethod.GET, null, String.class);
}
catch (HttpClientErrorException errorException){
logger.info("Status code :: {}, Exception message :: {} , response body ::{}" , e.getStatusCode()
e.getMessage(), e.getResponseBodyAsString());
}
catch (HttpStatusCodeException e){
logger.info("Status code :: {}, Exception message :: {} , response body ::{}" , e.getStatusCode()
e.getMessage(), e.getResponseBodyAsString());
}
For that you have to create RestTemplateError handler and register that class while creating bean for RestTemplate.
#Bean
public RestTemplate getBasicRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new RestTemplateResponseErrorHandler());
return restTemplate;
}
where your handler class has to implements ResponseErrorHandler. You can read the json response that is stored in the body.
#Component
public class RestTemplateResponseErrorHandler implements ResponseErrorHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(RestTemplateResponseErrorHandler.class);
#Override
public boolean hasError(ClientHttpResponse httpResponse) throws IOException {
return httpResponse.getStatusCode().series() == CLIENT_ERROR
|| httpResponse.getStatusCode().series() == SERVER_ERROR;
}
#Override
public void handleError(ClientHttpResponse httpResponse) throws IOException {
if (httpResponse.getStatusCode().series() == SERVER_ERROR) {
LOGGER.error("Handling server error response statusCode:{} ", httpResponse.getStatusCode());
} else if (httpResponse.getStatusCode().series() == CLIENT_ERROR) {
LOGGER.error("Handling Client error response statusCode:{} ", httpResponse.getStatusCode());
String body;
InputStreamReader inputStreamReader = new InputStreamReader(httpResponse.getBody(),
StandardCharsets.UTF_8);
body = new BufferedReader(inputStreamReader).lines().collect(Collectors.joining("\n"));
throw new CustomException(httpResponse.getStatusCode().toString(), httpResponse, body);
}
}
}

SpringBoot doesn't handle org.hibernate.exception.ConstraintViolationException

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));
}
}

Add a body to a 404 Not Found Exception

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");

Categories