Http Error Messages Not Propagating from Microservice to Webapp - java

I'm building a spring boot microservice-based application and I'm having trouble getting my error messages to propagate from the services containing all my business logic back into the webapp. In my services I'm throwing exceptions that look like this:
#ResponseStatus(HttpStatus.BAD_REQUEST)
public class Http400ServiceException extends Exception {
public Http400ServiceException() {
super("Some error message");
}
}
Everything behaves as expected, sending the response code as expected. So if my service sent a 403 exception, I'd get a 403 in the webapp. What I'm trying to do now is to get the error messages from the exceptions in my service.
The Problem
When I poke my services from a rest client in such a way as to produce a 403, the response (in JSON) looks like this:
{
"timestamp": 1459453512220
"status": 403
"error": "Forbidden"
"exception": "com.mysite.mypackage.exceptions.Http403ServiceException"
"message": "Username Rob must be between 5 and 16 characters long"
"path": "/createLogin"
}
However, for some reason I can't access the 'message' field from my webapp. I have some generic error handling code in the webapp that looks like this:
#Override
public RuntimeException handleException(Exception e) {
...
if (e instanceof HttpClientErrorException) {
HttpClientErrorException httpError = (HttpClientErrorException) e;
LOGGER.info(httpError.getResponseBodyAsString());
}
...
}
But when I look in my logs/run my app in debug, httpError.getResponseBodyAsString() is returning null. It's got the right response code, just no response body.
If anyone has any insights into what's going wrong, I'd really appreciate it.

So we parked this issue for a few months while we were working on other areas of the app. But just in case someone else sees this while trying to solve a similar problem, the approach I ended up taking was the following
Create an interface for all responses from all services to implement, and an exception type to indicate that the exception is because of user error:
public interface IModel {
boolean isError();
UserException getError();
}
In the controllers for each service, catch any exceptions and create some sort of IModel out of them:
#ResponseStatus(HttpStatus.OK)
#ExceptionHandler(UserException.class)
public IModel handleException(UserException exception) {
return exception.toModel();
}
In the component used to call the services, if the response has a UserException on it, throw it, otherwise return the response body:
public <T extends IModel> T makeCall(Object payload, Endpoint<T> endpoint) throws UserException {
...
ResponseEntity<T> response = restTemplate.postForEntity(endpoint.getEndpointUrl(), payload, endpoint.getReturnType());
if (response.getBody().isError()) {
throw response.getBody().getError();
}
...
return response.getBody();
}
In the webapp, handle the exceptions with the appropriate #ExceptionHandler e.g.
#ExceptionHandler(UserException.class)
public ModelAndView handleUserException(UserException e) {
ModelAndView modelAndView = new ModelAndView("viewName");
modelAndView.addObject("errorMessage", e.getMessage());
return modelAndView;
}
I kinda feel like there's a cleaner approach but this is the best I've been able to come up with so far. I'll update this if I find a better way of doing it though.

Related

Java Spring Boot: Exception Handling

A little new to the Java Spring Boot flavor of webservices -- so please be gentle. Why do most Spring Boot Controller examples not show any exceptions being captured? I see some of my fellow developers do this a lot. And the answer is: it's a common convention. But why. Am I missing a key concept of web services created using Spring Boot?
For example:
#PostMapping(path = "/sampleEndpoint/v1/myEndpoint", produces = "application/json")
public ResponseEntity<String> myEndpoint(#RequestBody MyEndpointRequest myEndpointRequest) {
MyEndpointResponse response = someService.myEndpoint(myEndpointRequest);
return new ResponseEntity<>(response, HttpStatus.OK);
}
I would think, with respects to the architecture you would add AT LEAST a try/catch block with say some logging, and throw a new exception with the exceptions message:
#PostMapping(path = "/sampleEndpoint/v1/myEndpoint", produces = "application/json")
public ResponseEntity<String> myEndpoint(#RequestBody MyEndpointRequest myEndpointRequest) {
try{
MyEndpointResponse response = someService.myEndpoint(myEndpointRequest);
return new ResponseEntity<>(response, HttpStatus.OK);
}catch(Exception ex){
//Your favorite logger:
log.error("STACK_TRACE: {}", StaticClass.stackTraceToString(ex));
//throw exception for calling or consuming system/application:
throw new MiscException(ex.getMessage());
}
}
A couple of things to give context to this question (observation):
Use multiple data sources: a couple of databases, and some other web services (gives our client a one stop place to get their data.
Using this webservice with potentially 4 different client side /presentation layer type of applications.
My team would like to capture unexpected exceptions emanating from the data sources we tap into...and log them.
Well it's up to developer to implement catch of exceptions mechanism. But it's a good practise to define exceptions types and error codes/messages for that. Let's say you have an endpoint which fetch product with id, but there is no product with that id, in that case client will receive http 500 code with internal server error message. This will confuse users and also developers, what was the real cause of that error.
So prevent those, you can get help from #ControllerAdvice annotation, which will allow to apply exception handlers to more than one or all controllers.
First you will define your custom exceptions like :
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(Long id) {
super(String.format("Product with id %d not found", id));
}
}
and then you can define your ControllerAdvice class:
#ControllerAdvice
public class ExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(ProductNotFound.class)
public ResponseEntity<Object> handleProductNotFoundException(
ProductNotFoundException ex, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("message", "Product not found");
return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
}
}

How can I set up my application to ONLY return messages for ResponseStatusException?

By default, Spring Boot does not return messages for any exceptions, including ResponseStatusException, meaning that the message about bar below will not be returned to the client:
#GetMapping("/foo")
#ResponseBody
public Foo getFoo(#RequestParam(name = "bar", defaultValue = "0") int bar) {
if (bar <= 0) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "bar must always be positive");
}
return example.getFoo(bar);
}
This can be changed by setting server.error.include-message=always in the application.properties, however this causes ALL exception messages to be returned to the client, including this one:
#GetMapping("/baz")
#ResponseBody
public Baz getBaz() {
if (!security.checkSecurity()) {
throw new RuntimeException("Security breach! Hope no one finds out!");
}
return example.getBaz();
}
I know this is a trivial example and the solution would be just "don't throw server exceptions from your controller", but the exception might actually come from some other code buried deep in the application, it could even be a NullPointerException or whatever.
How can I get the application to show messages only from ResponseStatusException and not other types of exception? (I guess other than adding try-catch clauses to every single controller method.)
You can add extra (#ExceptionHandler) methods to any controller to specifically handle exceptions thrown by request handling (#RequestMapping) methods in the same controller. Such methods can:
Handle exceptions without the #ResponseStatus annotation (typically predefined exceptions that you didn’t write)
Redirect the user to a dedicated error view
Build a totally custom error response
Controller advice allows you to use exactly the same exception handling techniques but apply them across the whole application, not just to an individual controller. You can think of them as an annotation-driven interceptor.
Any class annotated with #ControllerAdvice becomes a controller-advice and three types of method are supported:
Exception handling methods annotated with #ExceptionHandler.
Model enhancement methods (for adding additional data to the model) annotated with #ModelAttribute. Note that these attributes are not available to the exception handling views.
Binder initialization methods (used for configuring form-handling) annotated with
#InitBinder.
Solution:
#ControllerAdvice
public class RestControllerAdvice {
#ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<String> handleStatusException(ResponseStatusException exception) {
throw exception;
}
#ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception exception) {
return new ResponseEntity<>("Exception", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Catch all the exceptions and in the catch block throw ResponseStatusException like in:
#GetMapping("/actor/{id}")
public String getActorName(#PathVariable("id") int id) {
try {
return actorService.getActor(id);
} catch (ActorNotFoundException ex) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "Actor Not Found", ex);
}
}

Throw custom Exception with HTTP status code for Jackson Custom deserializer

I have this InstantDesrializer
#Slf4j
public class InstantDeserializer extends StdDeserializer<Instant> {
public InstantDeserializer() {
this(null);
}
public InstantDeserializer(Class<?> vc) {
super(vc);
}
#Override
public Instant deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
log.info(node.asText());
TemporalAccessor parse = null;
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(Constants.DATE_TIME_FORMAT).withZone(ZoneOffset.UTC);
try {
parse = dateTimeFormatter.parse(node.asText());
} catch (Exception e) {
e.printStackTrace();
throw new IOException();
}
log.info(Instant.from(parse).toString());
return Instant.from(parse);
}
}
And then corresponding IOException in #ControllerAdvice
#ExceptionHandler(IOException.class)
public ResponseEntity<String> handleIOException(IOException e) {
return ResponseEntity.status(422).build();
}
And this in my DTO:
#NotNull
#JsonDeserialize(using = InstantDeserializer.class)
// #DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private Instant timestamp;
Even when uncommented #DateTimeFormat, it's not working
Ideally, it should return 422 status. But, it returns 400.
Maybe I'm just missing something so small, which I'm not able to figure out.
This approach was suggested here:
Throw custom exception while deserializing the Date field using jackson in java
Your Controller method is never called because JSON body parsing threw an exception.
Your #ContollerAdvice is not applied because the Controller method is not called.
Your handleIOException method is not called, and your 422 status is not applied.
I suspect this is the situation in more detail...
An HTTP request includes a json body.
The controller method matching the #RequestMapping and other annotations of the request takes an instance of your DTO Class as a parameter.
Spring attempts to deserialize the incoming json body before calling your controlling method. It must do this in order to pass the DTO object.
Deserialization uses your custom deserializer which throws an IOException.
This IOException occurs before your controller method is called. In fact, your controller method is never called for this request.
Spring handles the exception using its default behavior, returning an HTTP 400. Spring has a very broad RFC 7231 conception of HTTP 400.
Since your controller method is never called, the #ControllerAdvice is never applied and your #ExceptionHandler does not see the exception. Status is not set to 422.
Why do I believe this?
I frequently see this kind of behavior from Spring, and I think it is the expected behavior. But I have not tracked down the documentation or read the source to be sure.
What can you do about it?
A simple approach which you may not like is to declare your controller method to take inputs that almost never fail, such as String.
You take over the responsibility for validating and deserializing inputs, and you decide what status and messages to return.
You call Jackson to deserialize. Your #ExceptionHandler methods are used.
Bonus: You can return the text of Jackson's often useful parse error messages. Those can help clients figure out why their json is rejected.
I would not be surprised if Spring offers a more stylish approach, a class to be subclassed, a special annotation. I have not pursued that.
What should you do about it?
400 vs. 422 is a case I prefer not to litigate. Depending on your priorities it might be best to accept Spring's convention.
RFC 7231 On Status 400
The 400 (Bad Request) status code indicates that the server cannot or
will not process the request due to something that is perceived to be
a client error (e.g., malformed request syntax, invalid request
message framing, or deceptive request routing).
If the HTTP status code police pick on you, you could point to this and say "I perceive this input to be a client error." Then argue that 422 is inappropriate unless you are serving WebDAV just to keep them off balance.
You do not need an handleIOException method, just add #ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
to your CustomException.
import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
#ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
public class MyException extends JsonProcessingException {
public MyException(String message) {
super(message);
}
}
So when you make invalid request
with body
{"timestamp":"2018-04-2311:32:22","id":"132"}
Response will be:
{
"timestamp": 1552990867074,
"status": 422,
"error": "Unprocessable Entity",
"message": "JSON parse error: Instant field deserialization failed; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Instant field deserialization failed (through reference chain: TestDto[\"timestamp\"])"
}
With valid request works fine:
{"timestamp":"2018-04-23T11:32:22.213Z","id":"132"}
Response:
{
"id": "132",
"timestamp": {
"nano": 213000000,
"epochSecond": 1514700142
}
}
The exception thrown by Jackson in case of a deserialization error is HttpMessageNotReadableException.
In your custom deserializer, you can throw your own deserialization exception which extends JsonProcessingException.
In your ControllerAdvice you can handle the HttpMessageNotReadableException and get the cause of this which is your custom exception.
This way, you can throw the http code you want.
#ExceptionHandler({HttpMessageNotReadableException.class})
#ResponseBody
public ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex) {
Throwable cause = ex.getCause();
if (cause.getCause() instanceof YourOwnException) {
//Return your response entity with your custom HTTP code
}
//Default exception handling
}

How can I handle exceptions with Spring Data Rest and the PagingAndSortingRepository?

Let's say I have a repository like:
public interface MyRepository extends PagingAndSortingRepository<MyEntity, String> {
#Query("....")
Page<MyEntity> findByCustomField(#Param("customField") String customField, Pageable pageable);
}
This works great. However, if the client sends a formed request (say, searching on a field that does not exist), then Spring returns the exception as JSON. Revealing the #Query, etc.
// This is OK
http://example.com/data-rest/search/findByCustomField?customField=ABC
// This is also OK because "secondField" is a valid column and is mapped via the Query
http://example.com/data-rest/search/findByCustomField?customField=ABC&sort=secondField
// This throws an exception and sends the exception to the client
http://example.com/data-rest/search/findByCustomField?customField=ABC&sort=blahblah
An example of the exception thrown and sent to client:
{
message:null,
cause: {
message: 'org.hibernate.QueryException: could not resolve property: blahblah...'
}
}
How can I handle those exceptions? Normally, I use the #ExceptionHandler for my MVC controllers but I'm not using a layer between the Data Rest API and the client. Should I?
Thanks.
You could use a global #ExceptionHandler with the #ControllerAdvice annotation. Basically, you define which Exception to handle with #ExceptionHandler within the class with #ControllerAdvice annotation, and then you implement what you want to do when that exception is thrown.
Like this:
#ControllerAdvice(basePackageClasses = RepositoryRestExceptionHandler.class)
public class GlobalExceptionHandler {
#ExceptionHandler({QueryException.class})
public ResponseEntity<Map<String, String>> yourExceptionHandler(QueryException e) {
Map<String, String> response = new HashMap<String, String>();
response.put("message", "Bad Request");
return new ResponseEntity<Map<String, String>>(response, HttpStatus.BAD_REQUEST); //Bad Request example
}
}
See also: https://web.archive.org/web/20170715202138/http://www.ekiras.com/2016/02/how-to-do-exception-handling-in-springboot-rest-application.html
You could use #ControllerAdvice and render the content your way. Here is tutorial if you need know how to work on ControllerAdvice, just remember to return HttpEntity

Spring Rest: Handling client input errors identified at Domain layer

I have a REST service for accounts. The controller calls the Service layer to find Accounts. AccountService can throw domain exceptions. It is usually a case related to client input error.In such a situation, I want to wrap the domain exception with ClientException. Is there a way that client can be presented with status code 400 and just the exception message? Or is there a better to handle the situation where the service layer detects an illegal argument?
#Controller
class AccountController
#ResponseBody
#RequestMapping("/accounts/${accountId}")
public Account account(#PathVariable int accountId, HttpServletResponse response){
try{
return accountService.find("Account not found with id: " + accountId);
}catch(Exception e){
response.setStatus(400);
throw new ClientException(e);
}
}
}
class AccountService{
public Account find(int accountId){
if(acountId > 100){
throw new AccountNotFoundException(accountId);
}else{
return new Account(accountId, "someone");
}
}
}
If this is a REST service, then simply setting the response status and a JSON body (with error message) should be enough. There's no real need to throw that ClientException.
You can create an exception handler that takes the exception and marshals it into a JSON response object.
I did this some time ago, and it worked fine, however I know longer have the source-code to share. .
Look at Spring MVC's exception handlers as a starting point.

Categories