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
Related
So I have a REST controller and I'm trying to catch the message coming from the IllegalStateException
#GetMapping
public List<Student> getAllStudent(){
throw new IllegalStateException("Opps can not get all students");
//return studentService.getAllStudents();
}
In my front end I want to log the message "Opps can not get all students" When I use Postman I get this JSON response:
{
"timestamp": "2021-08-29T22:06:47.477+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/api/students"
}
How do I add a message attribute to the JSON so I can use it in my front end?
Throw an exception as you like (YourselfException) such as "Can not get all students Exception"
then you can define a GlobalExceptionHandler and you can return the message you want
For more information, you can try search "GlobalExceptionHandler"
#RestControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(value = YourselfException.class)
#ResponseBody
public Response bizExceptionHandler(YourselfException e){
return Response.fail(e.getMessage);
}
}
I have this method to handle all request header missing exceptions, but in one controller is expected to receive a json as body. If it is a invalid json or is null, it drops a exception with custom messages:
#ExceptionHandler(value = {ServletRequestBindingException.class, HttpMessageNotReadableException.class})
public final ResponseEntity<ErrorResponse> handleHeaderException(Exception ex) {
List<String> details = new ArrayList<>();
details.add(ex.getMessage());
return new ResponseEntity<>(new ErrorResponse("Bad Request", details), HttpStatus.BAD_REQUEST);
}
{
"message": "Bad Request",
"details": [
"Required request body is missing: public org.springframework.http.ResponseEntity
packages.fazerLogin(packages.BodyLogin) throws java.io.IOException"
] }
{
"message": "Bad Request",
"details": [
"JSON parse error: Unexpected character ('\"' (code 34)): was expecting comma to separate Object entries; nested exception is
com.fasterxml.jackson.core.JsonParseException: Unexpected character
('\"' (code 34)): was expecting comma to separate Object entries\n at
[Source: (PushbackInputStream); line: 3, column: 3]"
] }
But I don't want a long message such as above. Just "Required request body" or "JSON parse error" is just fine. I want to know what can I do.
My controller:
#PostMapping(value = "v1/token", consumes = "application/json;charset=UTF-8")
public ResponseEntity<TokenOutputDto> doLogin(#RequestBody #Valid BodyLogin body) throws IOException {
return authenticationModel.auth(body.getEmail(), body.getPassword());
}
Also, should I create a #ExceptionHandler method to each one of the possibles exceptions (HttpClientErrorException, HttpServerErrorException etc)? It will be a bad pratice, because the code will repeat almost identically...
You can handle those exception handlers in your method by following way:
#ExceptionHandler(value = {ServletRequestBindingException.class, HttpMessageNotReadableException.class})
public final ResponseEntity<ErrorResponse> handleHeaderException(Exception ex) {
List<String> details = new ArrayList<>();
if (ex instanceof IOException ) {
if (ex.getCause() instanceof JsonParseException) {
details.add("JSON parse error");
} else {
details.add("Required request body");
}
} else {
details.add(ex.getMessage());
}
return new ResponseEntity<>(new ErrorResponse("Bad Request", details), HttpStatus.BAD_REQUEST);
}
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.
We have a microservice architecture. Each service exposing data through Rest. All controllers are set up using Spring:
#RestController
#RequestMapping(path = "foobar")
public class UiController {
#PostMapping("foo")
public ResponseEntity<Foo> addFoo(#RequestBody final FooDto fooDto) {
Foo fromDb = adminService.addFoo(converterToModel.convert(fooDto);
return ResponseEntity.ok(converterToDto.convert(fromDb));
}
If for some reason fooDto can't be added to the database. A custom Exception is thrown:
#ResponseStatus(value = HttpStatus.CONFLICT)
public class FooAlreadyAssignedException extends RuntimeException {
public FooAlreadyAssignedException(String msg) {
super("The following foos are already assigned to foobar: " + msg);
}
}
In Postman you see the following JSON after the Exception above is thrown
{
"timestamp": 1508247298817,
"status": 409,
"error": "Conflict",
"exception": "com.foo.exception.FooCodeAlreadyExistsException",
"message": "A foo with code: foo already exists",
"path": "/foo/foobar"
}
We have 4 different services like these all set up the same way.
Our UI is made in Angular 4 and makes REST calls to our Gateway. The Gateway is the connection between the microservices and the UI. It also exposes a REST endpoint. It's also implemented with Spring. I added a picture for clarification:
architecture
"edit: I see that I didn't complete the arrows. Of course all data is passed back up to the UI"
The problem
The Gateway uses a RestTemplate to call the APIs of the microservices
when a custom Exception is thrown in the microservice the Gateway returns this:
{
"timestamp": "2017-10-16T15:30:03.456Z",
"status": 500,
"error": "Internal Server Error",
"exception": "org.springframework.web.client.HttpClientErrorException",
"message": "409 null",
"path": "/v1/printstations"
}
My original response a HttpStatus.conflict (status = 409) seems to be wrapped in a status 500 message by the Gateway. I don't want this behavior, I want it to pass the original message to the UI.
Any ideas on how to control this behavior?
Notes
I have tested with Postman that if you access the microservice directly it returns the 409 with the message written in the custom Exception
I have already tried overriding Springs ResponseErrorHandler but was not able to find a suitable solution that way.
In gateway code where spring rest template is calling your microservices, I would recommend catching HttpClientErrorException and then create your own exception class like ApiException as in below example, this way you will be able to pass the exact exception which is thrown from the microservices:
catch (org.springframework.web.client.HttpClientErrorException e) {
throw new ApiException(e.getMessage(), e, e.getRawStatusCode(), e.getResponseHeaders(),
e.getResponseBodyAsString(), fullURIPath, null);
}
where ApiException has a constructor like below:
public ApiException(String message, Throwable throwable, int code, Map<String, List<String>> responseHeaders,
String responseBody, String requestURI, String requestBody) {
super(message, throwable);
this.code = code;
this.responseHeaders = responseHeaders;
this.responseBody = responseBody;
this.requestURI = requestURI;
this.requestBody = requestBody;
}
Issue can be closed.
The solution was to map the exception that happened in the microservice to a valid ResponseEntity in the Gateway, so that the RestTemplate in the Gateway wouldn't repackage the error in a 500 server error.
We did this by creating a #ControllerAdvice class
#ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(value = {HttpClientErrorException.class})
protected ResponseEntity<Object> handleConflict(HttpClientErrorException ex, WebRequest request) {
return handleExceptionInternal(ex, ex.getResponseBodyAsString(),
new HttpHeaders(), ex.getStatusCode(), request);
}
}
This results in a ResponseEntity with the correct HttpStatus from the Exception in the microservice and a JSON body containing the message needed by the front-end.
I am developping an single page app with angularjs and Spring Mcv Rest.
I am calling my service (mail sending with javax mail) like that in Angularjs : SendProformaFax.get({idCommande:$scope.commande.id})
And on server side my service :
#RequestMapping(value = "/sendProformaFax/{idCommande}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#Timed
public void imprimeProforma(#PathVariable String idCommande) {
Commande commande = commandeRepository.findOne(new Long(idCommande));
List<Vente> ventes = venteRepository.findAllByCommande(commande);
blService.sendProformaFax(ventes);
}
I would like to display a message when the function sendProformaFax throws a MessagingException.
I don't know how to return this exception in my RestController and how to catch it in Angularjs.
If anyone can help me on this...
Thanks.
EDIT :
On server side I am doing this :
#ExceptionHandler(value = Exception.class)
public ErrorView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
// If the exception is annotated with #ResponseStatus rethrow it and let
// the framework handle it - like the OrderNotFoundException example
// at the start of this post.
// AnnotationUtils is a Spring Framework utility class.
if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null)
throw e;
// Otherwise setup and send the user to a default error-view.
ErrorView mav = new ErrorView();
mav.setException(e.getMessage());
mav.setUrl(req.getRequestURL().toString());
mav.setMessage("Veuillez contacter le support informatique.");
return mav;
}
On Angularjs side I am doing this
CreateFichierCiel.get({params:param}, function (response) {
$scope.infoMessage = "La génération du fichier CIEL est terminée."
$activityIndicator.stopAnimating();
$("#messageModal").modal('show');
$scope.find();
}, function (reason) {
$("#errorModal").modal('show');
})
But 'reason' object is like this :
config: Object data: Object error: "Internal Server Error" exception:
"java.lang.NullPointerException" message: "No message available" path:
"/api/createFichierCiel/15-00005" status: 500 timestamp: 1438430232307
proto: Object headers: function (name) { status: 500 statusText:
"Internal Server Error" proto: Object
So I am not getting the ErrorView class sent from the server.
If anyone can see where I am wrong here...
Thanks
You can make ExceptionHandler for MessagingException and set HTTPStatus to indicate that response has an error (egz. BAD_REQUEST)
#ExceptionHandler(MessagingException.class)
#ResponseStatus(HTTPStatus.BAD_REQUEST)
#ResponseBody
public ErrorView handleMessagingException(MessagingException ex) {
// do something with exception and return view
}
In AngularJS you can catch it from resource service like this:
MessagingService.get({idCommande: 1}, function (data) {
// this is success
}, function (reason) {
// this is failure, you can check if this is a BAD_REQUEST and parse response from exception handler
};
It almost the same when you use $http.
Adding to the answer by kTT, starting with Spring 4 you can wrap your #ExceptionHandler method in a class annotated with #ControllerAdvice so that you will have the same message for the same type of exception across the whole application. More you can look here
That is how I did it, we are using spring mvc and angularjs in our project.
I have this controllerAdvice class
#ControllerAdvice
public class ExceptionControllerAdvice {
#ExceptionHandler(ServiceException.class)
public ResponseEntity<ErrorResponse> rulesForCustomerNotFound(HttpServletRequest req, ServiceException e)
{
ErrorResponse error = new ErrorResponse();
error.portalErrorCode = e.getExceptionCode();
error.message = e.getMessage();
return new ResponseEntity<ErrorResponse>(error, HttpStatus.NOT_FOUND);
}
}
class ErrorResponse {
public int portalErrorCode;
public String message;
}
and then in restful controller where ServiceException is a customized runnable exception:
#Override
#RequestMapping("/getControls/{entity}")
public List<Control> getControls(#PathVariable(value="entity") String entity) throws ServiceException {
List<Control> controls = ImmutableList.of();
try {
controls = dao.selectControls(entity);
} catch (Exception e) {
logger.error("getting list of controls encountered an error ", e);
throw new ServiceException(50, "getting list of controls encountered an error.");
}
return controls;
}
in my app.js file in angularjs I use
.config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push(function ($q, $location) {
return {
'response': function (response) {
//Will only be called for HTTP up to 300
return response;
},
'responseError': function (rejection) {
if(rejection.status === 0) {
alert('There is a problem connecting to the server. Is the server probably down?!');
}
else {
$location.url('/error').search({rejection: rejection});
}
return $q.reject(rejection);
}
};
});
}])
and in a error.controller.js
function init() {
ctrl.rejection = $location.search().rejection;
ctrl.portalErrorCode = ctrl.rejection.data.portalErrorCode;
ctrl.errorMessage = ctrl.rejection.data.message;
$log.info('An error occured while trying to make an ajax call' + ctrl.errorMessage + ': ' + ctrl.portalErrorCode);
}
and of course in error.tpl.html
<h2>
{{ctrl.rejection.status}} {{ctrl.rejection.statusText}}
</h2>
<h3 class="error-details">
Sorry, an error has occurred!
</h3>
<h3 class="error-details">
{{ctrl.errorMessage}}
</h3>