Post Thumbnail

Monolith Spring Boot Exception Handling

1. Overview

This article is a build up on the first article that explores the basics of Spring Boot exception handling and explains the different techniques used. The first article used an API server that returns JSON format for example throughout.

In this follow-up article, however, we will be working with HTML response and be returning web pages instead of JSON. We will be using the same technique and so it’s highly recommended that you read the first article before continuing with this.

2. Application Setup

The application we will be working with contains spring-boot-starter-web and spring-boot-starter-freemarker as main dependencies. We can generate the bare scaffolding using this Spring Initializr link.

3. Basic Exception Handling

By default, Spring Boot will bootstrap the BasicErrorController to handle all calls to the /error endpoint. Whenever there’s an error while accessing the application from a web browser the errorHtml function of the BasicErrorController will be called since the Accept content-type is text/html.

Furthermore, the errorHtml method will use available error view resolvers to get the right error page. The DefaultErrorViewResolver will look for suitable view files in the resources/templates/error, resources/static/error and resources/error directories and display the first match to the end-user.

This simplifies things for us a lot! All we have to do to handle a 404 error is to provide a 404.ftlh file in the resources/templates/error directory and it will be served whenever there’s a 404 error.

Listing 3.1 resources/templates/error/404.ftlh

1
2
3
4
5
6
7
8
<html>
<head>
    <title>404 Error</title>
</head>
<body>
    <h1>Resource not found</h1>
</body>
</html>

We can repeat this technique for all other error status codes like 403, 500 and 503. The reason we’re using the .ftlh extension is that we’re using the Freemarker template engine and that’s the default file extension.

If we’re using other template engines the view files will have a different extension. For example, the Thymeleaf template engine uses the .html extension by default.

In addition to individual error view files, we can also create a catch-all error file in resources/templates/error.ftlh. This will be the fallback if no other specific error views are found.

Let’s create the resource file and an endpoint that will intentionally throw a runtime exception, so we can see it work.

Listing 3.2 resources/templates/error.ftlh

1
2
3
4
5
6
7
8
<html>
<head>
    <title>General Error</title>
</head>
<body>
<h1>Something went wrong internally</h1>
</body>
</html>

Listing 3.3 IndexController.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Controller
public class IndexController {

    @GetMapping 
    public String index() {
        return "index";
    }

    @GetMapping("/ex/runtime")
    public String runtimeEx() {
        throw new RuntimeException("Runtime Exception");
    }
}

If we visit the /ex/runtime endpoint, we will see the content of resources/templates/error.ftlh, because we did not define a specific resources/templates/error/500.ftlh error page.

4. Custom Error Controller

There are times that we want to do something more than just return a view when an exception occurred. For this purpose, we can define our own error handler that will be called instead of the default basic error controller.

Our custom error handler is a controller class, i.e. annotated with @Controller, that has a request mapping for the /error endpoint. The class must also implement the ErrorController interface so that it will replace the default BasicErrorController component.

Listing 4.1 GlobalExceptionHandler.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Controller
public class GlobalExceptionHandler implements ErrorController {

    @RequestMapping("/error")
    public String handleError(HttpServletRequest request, Model model) {
        //do something custom and return a custom view
        model.addAttribute("whatnext", "Try again in " +
                LocalDateTime.now().plusMinutes(10));
        return "error/custom-error";
    }
}

Our implementation is also similar to the default one, it’s just that we can now execute custom logic and add custom attributes to be used in the view template.

Listing 4.2 resources/templates/error/custom-error.ftlh

1
2
3
4
5
6
7
8
9
<html>
<head>
    <title>Custom Error Page</title>
</head>
<body>
    <h1>A custom error message</h1>
    <p>Next Step: ${whatnext}</p>
</body>
</html>

This is one of the sweet points of Spring Boot. In as much as there are sensible defaults, we can easily use custom implementations and everything will still work seamlessly.

Always remember to exclude your /error endpoint from Spring Security to avoid access denied error when Spring Boot is redirecting to the /error endpoint. Ensure you set the permission level to permit all.

5. Handling Specific Exception Classes

In part one of this series, we learnt that it’s possible to create a specific exception class that we can throw from any part of the application and it will be handled by a designated method.

We can apply the same technique here as well, although we will be returning a specific error page instead of a JSON.

To achieve this, we will annotate our existing GlobalExceptionHandler.java with @ControllerAdvice and add a new method to handle the CustomApplicationException class.

Listing 5.1 CustomApplicationException.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Controller
@ControllerAdvice
public class GlobalExceptionHandler implements ErrorController {

    @ExceptionHandler(CustomApplicationException.class)
    public String handCustomEx(CustomApplicationException ex, Model model) {
        model.addAttribute("code", ex.getHttpStatus().value());
        model.addAttribute("message", ex.getMessage());
        model.addAttribute("errors", ex.getErrors());
        return "error/custom-app-error";
    }

    @RequestMapping("/error")
    public String handleError(HttpServletRequest request, Model model) {
        //do something custom and return a custom view
        model.addAttribute("whatnext", "Try again in " +
                LocalDateTime.now().plusMinutes(10));
        return "error/custom-error";
    }
}

Let’s create a separate error page to demonstrate this and a new endpoint to test it.

Listing 5.2 resources/templates/errors/custom-app-error.ftlh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<html>
<head>
    <title>Custom Application Error Page</title>
</head>
<body>
    <h1>A custom application error</h1>
    <p>Message: ${message}</p>
    <p>Code: ${code}</p>
    <p>Errors</p>
    <ul>
        <#list errors as error>
            <li>${error}</li>
        </#list>
    </ul>
</body>
</html>

Listing 5.3 IndexController.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@GetMapping("/ex/custom")
public ResponseEntity<Map<String, Object>> customException(@RequestParam Map<String, Object> request) {
    List<String> errors = new ArrayList<>();
    if(!request.containsKey("username"))
        errors.add("Username is required");

    if(!errors.isEmpty()) {
        String errorMessage = "Missing required parameters";
        throw new CustomApplicationException(HttpStatus.BAD_REQUEST, errorMessage , errors);
    }

    return ResponseEntity.ok(Collections.singletonMap("status", true));
}

This technique can also be applied to any specific exception classes that we desire to treat specially. Also, it’s important to note that once an exception has been handled by a specific method, it will not be handled by other handlers like the /error endpoint.

6. Conclusion

In this article, we’ve looked extensively at how to handle exceptions in Spring Boot and return corresponding error pages. The complete source code is available on GitHub.

Love this concise and interesting article about exception handling? You should totally check out my book on Spring Cloud OpenFeign. You’ll never call external APIs the same way again.