📜 ⬆️ ⬇️

JSON error handling with Spring Boot

Often, when working with microservices built using Spring Boot technology, one can see standard error output similar to this:

{ "timestamp": 1510417124782, "status": 500, "error": "Internal Server Error", "exception": "com.netflix.hystrix.exception.HystrixRuntimeException", "message": "ApplicationRepository#save(Application) failed and no fallback available.", "path": "/application" } 

Such a conclusion may be unnecessary and unnecessary customers of your service. If you want to simplify the life of third-party services in case of an error, then this is exactly what the post will deal with.

We begin with building a small service with one controller. Our service will receive a request to receive the user and, if successful, give the data to the user. In case of failure, an error is returned to us. Let's start with a simple and further in the article we will improve the project.

So, the first thing we need is a user:
')
 import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class User { private int id; private String firstName; private String lastName; } 

Here I used the lombok library. Data annotation substitutes getters and setters in a class. The remaining annotations add an empty constructor and a constructor with parameters. If you want to repeat this example in your IntelliJ Idea , then you need to tick the option enable annotation processing, or write everything by hand.

Next, we need a service (for the sake of brevity, we will not create a repository):

 import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; @Service public class UserService { private static final Map<Integer, User> userStorage = new HashMap<>(); static { userStorage.put(1, new User(1, "Petr", "Petrov")); userStorage.put(2, new User(2, "Ivan", "Ivanov")); userStorage.put(3, new User(3, "Sergei", "Sidorov")); } public User get(int id) { return userStorage.get(id); } } 

And, of course, the controller itself:

 import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("user") public class UserController { private UserService userService; @Autowired public UserController(UserService userService) { this.userService = userService; } @GetMapping("{id}") public User get(@PathVariable(name = "id") int id) { return userService.get(id); } } 

So, we have almost full service with users. Run it and look.

When requesting a localhost : 8080 / user / 1 URL, json is returned to us in this format:

 { "id": 1, "firstName": "Petr", "lastName": "Petrov" } 

All perfectly. But what happens if you make a request to the localhost : 8080 / user / 4 URL (we have only 3 users)? The correct answer is: we will get status 200 and nothing in the answer. The situation is not particularly pleasant. There is no error, but there is no requested object either.

Let's improve our service and add an error to it in case of failure. First, create an exception:

 public class ThereIsNoSuchUserException extends RuntimeException { } 

Now we add an error forwarding to the service:

 public User get(int id) { User user = userStorage.get(id); if (user == null) { throw new ThereIsNoSuchUserException(); } return user; } 

Let's restart the service and see again what will happen when a non-existing user is requested:

 { "timestamp": 1510479979781, "status": 500, "error": "Internal Server Error", "exception": "org.faoxis.habrexception.ThereIsNoSuchUserException", "message": "No message available", "path": "/user/4" } 

That's better. Much more informative and the status code is not 200. The client on his side is already able to successfully and easily handle this situation. But, as they say, there is one nuance. Errors can be completely different, and the client of our service will have to put a bunch of conditional operators and investigate what went wrong with us and how to fix it. It turns out a little rough on our side.

Just for such cases the ResponseStatus annotation was invented. Let's substitute it in the place of our exception and in practice let's see how it works:

 @ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "There is no such user") public class ThereIsNoSuchUserException extends RuntimeException { } 

Repeat the query and see the result:

 { "timestamp": 1510480307384, "status": 404, "error": "Not Found", "exception": "org.faoxis.habrexception.ThereIsNoSuchUserException", "message": "There is no such user", "path": "/user/4" } 

Fine! The status code and message have changed. Now the client will be able to decide on the answer code the reason for the error and even specify it by the message field. But still there is a problem. Most customer fields may simply not be needed. For example, the response code as a separate field may be redundant, since we already receive it with the response code. With this you need to do something.

Fortunately, with spring boot it is not so difficult to take the last step to our successful error notification.

All that is required is to parse a couple of annotations and one class:


Let's now see how to merge all this and build our unique and inimitable error message:

 import lombok.AllArgsConstructor; import lombok.Data; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; @ControllerAdvice public class AwesomeExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(ThereIsNoSuchUserException.class) protected ResponseEntity<AwesomeException> handleThereIsNoSuchUserException() { return new ResponseEntity<>(new AwesomeException("There is no such user"), HttpStatus.NOT_FOUND); } @Data @AllArgsConstructor private static class AwesomeException { private String message; } } 


Let's make the same request and see the 404 response status and our message with a single field:

 { "message": "There is no such user" } 

Annotation ResponseStatus above our exception can be safely removed.

As a result, we got an application in which error handling is configured as flexibly and simply as possible. The full project can be found in the github repository. I hope that everything was simple and clear. Thank you for your attention and write comments! I will be glad to your comments and clarifications!

Source: https://habr.com/ru/post/342214/


All Articles