Enterprise Java

Providing useful API error messages with Spring Boot

For API users it is quite important an API provides useful error messages. Otherwise, it can be hard to figure out why things do not work. Debugging what’s wrong can quickly become a larger effort for the client than actually implementing useful error responses on the server side. This is especially true if clients are not able to solve the problem themself and additional communication is required.

Nonetheless this topic is often ignored or implemented halfheartedly.

Client and security perspectives

There are different perspectives on error messages. Detailed error messages are more helpful for clients while, from a security perspective, it is preferable to expose as little information as possible. Luckily those two views often do not conflict that much, when implemented correctly.

Clients are usually interested in very specific error messages if the error is produced by them. This should usually be indicated by a 4xx status code. Here, we need specific messages that point to the mistake made by the client without exposing any internal implementation detail.

On the other hand, if the client request is valid and the error is produced by the server (5xx status codes), we should be conservative with error messages. In this case, the client is not able to solve the problem and therefore does not require any details about the error.

A response indicating an error should contain at least two things: A human readable message and an error code. The first one helps the developer that sees the error message in the log file. The later allows specfic error processing on the client (e.g. showing a specific error message to the user).

How to build a useful error response in a Spring Boot application?

Assume we have a small application in which we can publish articles. A simple Spring controller to do this might look like this:

01
02
03
04
05
06
07
08
09
10
11
@RestController
public class ArticleController {
 
    @Autowired
    private ArticleService articleService;
 
    @PostMapping("/articles/{id}/publish")
    public void publishArticle(@PathVariable ArticleId id) {
        articleService.publishArticle(id);
    }
}

Nothing special here, the controller just delegates the operation to a service, which looks like this:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@Service
public class ArticleService {
 
    @Autowired
    private ArticleRepository articleRepository;
 
    public void publishArticle(ArticleId id) {
        Article article = articleRepository.findById(id)
                .orElseThrow(() -> new ArticleNotFoundException(id));
 
        if (!article.isApproved()) {
            throw new ArticleNotApprovedException(article);
        }
 
        ...
    }
}

Inside the service we throw specific exceptions for possible client errors. Note that those exception do not just describe the error. They also carry information that might help us later to produce a good error message:

01
02
03
04
05
06
07
08
09
10
public class ArticleNotFoundException extends RuntimeException {
    private final ArticleId articleId;
 
    public ArticleNotFoundException(ArticleId articleId) {
        super(String.format("No article with id %s found", articleId));
        this.articleId = articleId;
    }
     
    // getter
}

If the exception is specific enough we do not need a generic message parameter. Instead, we can define the message inside the exception constructor.

Next we can use an @ExceptionHandler method in a @ControllerAdvice bean to handle the actual exception:

01
02
03
04
05
06
07
08
09
10
11
12
13
@ControllerAdvice
public class ArticleExceptionHandler {
 
    @ExceptionHandler(ArticleNotFoundException.class)
    public ResponseEntity<ErrorResponse> onArticleNotFoundException(ArticleNotFoundException e) {
        String message = String.format("No article with id %s found", e.getArticleId());
        return ResponseEntity
                .status(HttpStatus.NOT_FOUND)
                .body(new ErrorResponse("ARTICLE_NOT_FOUND", message));
    }
     
    ...
}

If controller methods throw exceptions, Spring tries to find a method annotated with a matching @ExceptionHandler annotation. @ExceptionHandler methods can have flexible method signatures, similar to standard controller methods. For example, we can a HttpServletRequest request parameter and Spring will pass in the current request object. Possible parameters and return types are described in the Javadocs of @ExceptionHandler.

In this example, we create a simple ErrorResponse object that consists of an error code and a message.

The message is constructed based on the data carried by the exception. It is also possible to pass the exception message to the client. However, in this case we need to make sure everyone in the team is aware of this and exception messages do not contain sensitive information. Otherwise, we might accidentally leak internal information to the client.

ErrorResponse is a simple Pojo used for JSON serialization:

01
02
03
04
05
06
07
08
09
10
11
public class ErrorResponse {
    private final String code;
    private final String message;
 
    public ErrorResponse(String code, String message) {
        this.code = code;
        this.message = message;
    }
 
    // getter
}

Testing error responses

A good test suite should not miss tests for specific error responses. In our example we can verify error behaviour in different ways. One way is to use a Spring MockMvc test.

For example:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
@SpringBootTest
@AutoConfigureMockMvc
public class ArticleExceptionHandlerTest {
 
    @Autowired
    private MockMvc mvc;
 
    @MockBean
    private ArticleRepository articleRepository;
 
    @Test
    public void articleNotFound() throws Exception {
        when(articleRepository.findById(new ArticleId("123"))).thenReturn(Optional.empty());
 
        mvc.perform(post("/articles/123/publish"))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.code").value("ARTICLE_NOT_FOUND"))
                .andExpect(jsonPath("$.message").value("No article with id 123 found"));
    }
}

Here, we use a mocked ArticleRepository that returns an empty Optional for the passed id. We then verify if the error code and message match the expected strings.

In case you want to learn more about testing spring applications with mock mvc: I recently wrote an article showing how to improve Mock mvc tests.

Summary

Useful error message are an important part of an API.

If errors are produced by the client (HTTP 4xx status codes) servers should provide a descriptive error response containing at least an error code and a human readable error message. Responses for unexpected server errors (HTTP 5xx) should be conservative to avoid accidental exposure any internal information.

To provide useful error responses we can use specific exceptions that carry related data. Within @ExceptionHandler methods we then construct error messages based on the exception data.

Published on Java Code Geeks with permission by Michael Scharhag, partner at our JCG program. See the original article here: Providing useful API error messages with Spring Boot

Opinions expressed by Java Code Geeks contributors are their own.

Michael Scharhag

Michael Scharhag is a Java Developer, Blogger and technology enthusiast. Particularly interested in Java related technologies including Java EE, Spring, Groovy and Grails.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Argus Bonvoy
3 years ago

Why are you marking local variables as private? They’re private by default.

Back to top button