Error Handling

Learn about Error handling in Micronaut.

Authors: Sergio del Amo

Micronaut Version: 1.0.0

1 Getting Started

In this guide, you will learn the different options to handle errors in Micronaut.

1.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

1.2 Solution

We recommend you to follow the instructions in the next sections and create the app step by step. However, you can go right to the completed example.

or

Then, cd into the complete folder which you will find in the root project of the downloaded/cloned project.

2 Writing the App

Create the microservice:

mn create-app example.micronaut.complete

2.1 Global @Error

We want to display a custom Not Found page when the user attempts to access a uri for which we have not any route defined for.

notfound

The views module provides support for view rendering on the server side and does so by rendering views on the I/O thread pool in order to avoid blocking the Netty event loop.

To use the view rendering features described in this section, add the following dependency on your classpath. For example, in build.gradle

build.gradle
    compile "io.micronaut:micronaut-views"
    runtime "org.apache.velocity:velocity-engine-core:2.0"

Micronaut ships out-of-the-box with support for Apache Velocity, Thymeleaf or Handlebars. In this guide, we use Apache Velocity.

Create a notfound.vm view:

src/main/resources/views/notfound.vm
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Not Found</title>
</head>
<body>
<h1>NOT FOUND</h1>

<p><b>The page you were looking for appears to have been moved, deleted or does not exist.</b></p>

<p>This is most likely due to:</p>

<ul>
    <li>An outdated link on another site</li>
    <li>A typo in the address / URL</li>
</ul>
</body>
</html>

Create a NotFoundController:

src/main/java/example/micronaut/NotFoundController.java
package example.micronaut;

import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Error;
import io.micronaut.http.hateos.JsonError;
import io.micronaut.http.hateos.Link;
import io.micronaut.views.ViewsRenderer;

import java.util.Collections;

@Controller("/notfound") (1)
public class NotFoundController {

    private final ViewsRenderer viewsRenderer;

    public NotFoundController(ViewsRenderer viewsRenderer) { (2)
        this.viewsRenderer = viewsRenderer;
    }

    @Error(status = HttpStatus.NOT_FOUND, global = true)  (3)
    public HttpResponse notFound(HttpRequest request) {
        if (request.getHeaders()
                .accept()
                .stream()
                .anyMatch(mediaType -> mediaType.getName().contains(MediaType.TEXT_HTML))) { (4)
            return HttpResponse.ok(viewsRenderer.render("notFound", Collections.emptyMap()))
                    .contentType(MediaType.TEXT_HTML);
        }

        JsonError error = new JsonError("Page Not Found")
                .link(Link.SELF, Link.of(request.getUri()));

        return HttpResponse.<JsonError>notFound()
                .body(error); (5)
    }
}
1 The class is defined as a controller with the @Controller annotation.
2 Inject an available ViewRenderer bean to render an HTML view.
3 The Error declares which HttpStatus error code to handle (in this case 404). We declare the method as a global error handler due to global = true.
4 If the request Accept HTTP Header contains text/html, we respond an HTML View.
5 By default, we respond JSON.

2.2 Local @Error

In this section, we are going to create a local error handling to display validation errors when a form submission fails.

Micronaut’s validation is built on with the standard framework – JSR 380, also known as Bean Validation 2.0.

Hibernate Validator is a reference implementation of the validation API.

Add a dependency to it:

build.gradle
    compile "io.micronaut.configuration:micronaut-hibernate-validator"

Then create a view to display a form:

createbook
src/main/resources/views/bookscreate.vm
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Create Book</title>
    <style type="text/css">
        form fieldset li {
            list-style-type: none;
        }
        #errors span { color: red; }
    </style>
</head>
<body>
<h1>Create Book</h1>
<form action="/books/save" method="post">
    <fieldset>
        <ol>
            <li>
                <label for="title">Title</label>
                <input type="text" id="title" name="title" value="$title"/>
            </li>
            <li>
                <label for="pages">Pages</label>
                <input type="text" id="pages" name="pages" value="$pages"/>
            </li>
            <li>
                <input type="submit" value="Save"/>
            </li>
        </ol>
    </fieldset>
</form>
#if( $errors )
    <ul id="errors">
        #foreach( $error in $errors )
            <li><span>$error</span></li>
        #end
    </ul>
#end
</body>
</html>

Create a controller to map the form submission:

src/main/java/example/micronaut/BookController.java
package example.micronaut;

import java.util.List;
import io.micronaut.core.io.Writable;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Consumes;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.annotation.Produces;
import io.micronaut.validation.Validated;
import io.micronaut.views.View;
import io.micronaut.views.ViewsRenderer;
import io.micronaut.http.annotation.Error;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Path;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

@Validated (1)
@Controller("/books") (2)
public class BookController {

    @View("bookscreate") (3)
    @Get("/create") (4)
    public Map<String, Object> create() {
        return createModelWithBlankValues();
    }

    @Consumes(MediaType.APPLICATION_FORM_URLENCODED) (5)
    @Post("/save") (6)
    public HttpResponse save(@Valid @Body CommandBookSave cmd) { (7)
        return HttpResponse.ok();
    }

    private Map<String, Object> createModelWithBlankValues() {
        final Map<String, Object> model = new HashMap<>();
        model.put("title", "");
        model.put("pages", "");
        return model;
    }
1 The class is defined as a controller with the @Controller annotation mapped to the path /books
2 Add @Validated annotation at the class level to any class that requires validation.
3 Use @View annotation to indicate the view name which should be used to render a view for the route.
4 You can specify the HTTP verb that a controller’s action responds to. To respond to a GET request, use the io.micronaut.http.annotation.Get annotation.
5 @Consumes annotation takes a String[] of supported media types for an incoming request.
6 The @Post annotation is used to map the index method to all requests that use an HTTP POST
7 Add @Valid to any method parameter which requires validation. We use a POGO to encapsulate the form submission.

Create the POJO encapsulating the submission:

src/main/java/example/micronaut/BookController.java
package example.micronaut;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;

public class CommandBookSave {
    @NotBlank (1)
    private String title;

    @NotNull (2)
    @Positive (3)
    private Integer pages;

    public CommandBookSave() {

    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Integer getPages() {
        return pages;
    }

    public void setPages(Integer pages) {
        this.pages = pages;
    }
}
1 title is required and must be not blank.
2 pages is required.
3 pages must be greater than 0.

When the form submission fails, we want to display the errors in the UI as the next image illustrates:

createbookserrors

An easy way to achieve it is to capture the javax.validation.ConstraintViolationException exception in a local @Error handler. Modify BookController.java:

src/main/java/example/micronaut/BookController.java
...
class BookController {
...
..
    private  final MessageSource messageSource;

    public BookController(MessageSource messageSource) {
        this.messageSource = messageSource;
    }
...
.
    @View("bookscreate")
    @Error(exception = ConstraintViolationException.class) (3)
    public Map<String, Object> onSavedFailed(HttpRequest request, ConstraintViolationException ex) { (4)
        final Map<String, Object> model = createModelWithBlankValues();
        model.put("errors", messageSource.violationsMessages(ex.getConstraintViolations()));
        Optional<CommandBookSave> cmd = request.getBody(CommandBookSave.class);
        cmd.ifPresent(bookSave -> populateModel(model, bookSave));
        return model;
    }

    private void populateModel(Map<String, Object> model, CommandBookSave bookSave) {
        model.put("title", bookSave.getTitle());
        model.put("pages", bookSave.getPages());
    }




..
...
}
1 Constructor injection
2 Annotate a controller’s action with @Produces to change the response content type.
3 By default @Error annotations are local. We specify the exception which we want to handle.
4 You can access the original HttpRequest which triggered the exception.

Create a javax.inject.Singleton to encapsulate the generation of a list of messages from a Set of ConstraintViolation:

src/main/java/example/micronaut/MessageSource.java
package example.micronaut;

import javax.inject.Singleton;
import javax.validation.ConstraintViolation;
import javax.validation.Path;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Singleton
public class MessageSource {

    public List<String> violationsMessages(Set<ConstraintViolation<?>> violations) {
        return violations.stream()
                .map(MessageSource::violationMessage)
                .collect(Collectors.toList());
    }

    private static String violationMessage(ConstraintViolation violation) {
        StringBuilder sb = new StringBuilder();
        Path.Node lastNode = lastNode(violation.getPropertyPath());
        if (lastNode != null) {
            sb.append(lastNode.getName());
            sb.append(" ");
        }
        sb.append(violation.getMessage());
        return sb.toString();
    }

    private static Path.Node lastNode(Path path) {
        Path.Node lastNode = null;
        for (final Path.Node node : path) {
            lastNode = node;
        }
        return lastNode;
    }
}

2.3 ExceptionHandler

Another mechanism to handle global exception is to use a ExceptionHandler.

Modify the controller and add a method to throw an exception:

src/main/java/example/micronaut/BookController.java
@Validated (1)
@Controller("/books") (2)
public class BookController {
...
..
.
    @Produces(MediaType.TEXT_PLAIN)
    @Get("/stock/{isbn}")
    public Integer stock(String isbn) {
        throw new OutOfStockException();
    }
}
src/main/java/example/micronaut/OutOfStockException.java
package example.micronaut;

public class OutOfStockException extends RuntimeException {
}

Implement a ExceptionHandler; a generic hook for handling exceptions that occurs during the execution of an HTTP request.

src/main/java/example/micronaut/OutOfStockExceptionHandler.java
package example.micronaut;

import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.server.exceptions.ExceptionHandler;
import javax.inject.Singleton;

@Produces
@Singleton (1)
@Requires(classes = {OutOfStockException.class, ExceptionHandler.class})  (2)
public class OutOfStockExceptionHandler implements ExceptionHandler<OutOfStockException, HttpResponse> { (3)

    @Override
    public HttpResponse handle(HttpRequest request, OutOfStockException exception) {
        return HttpResponse.ok(0); (4)
    }
}
1 To register a Singleton in Micronaut’s application context, annotate your class with javax.inject.Singleton.
2 This bean loads if OutOfStockException, ExceptionHandler are available.
3 Specify the Throwable to handle.
4 Return 200 OK with a body of 0; no stock.

3 Next Steps

Explore more features with Micronaut Guides.