Exception Handling and Validation Handler in Vertx applications
In the last post, we have built a simple RESTful APIs example application using Eclipse Vertx. In this post, we will discuss web related topic, such as exception handling and input validation, etc.
Vertx’s Future includes some hooks to be executed when the asynchronous flow is done.
- onComplete will in invoked when the execution is completed, either it is succeeded or failed.
- onSuccess handles the successful result.
- onFailure catches the exception thrown in the execution.
Let’s explore how to handle the exceptions in the former example application.
Assume retrieving Post via a none-existing id, throw a PostNotFoundException instead of returning the correct result.
Declare a PostNotFoundException .
public class PostNotFoundException extends RuntimeException {
public PostNotFoundException(UUID id) {
super("Post id: " + id + " was not found. ");
}
}
In the PostRepository, change the content of findById like the following.
public Future<Post> findById(UUID id) {
Objects.requireNonNull(id, "id can not be null");
return client.preparedQuery("SELECT * FROM posts WHERE id=$1")
.execute(Tuple.of(id))
.map(RowSet::iterator)
.map(iterator -> {
if (iterator.hasNext()) {
return MAPPER.apply(iterator.next());
}
throw new PostNotFoundException(id);
});
}
In the PostsHandler, the get method handles /posts/:id route like this.
public void get(RoutingContext rc) {
var params = rc.pathParams();
var id = params.get("id");
this.posts.findById(UUID.fromString(id))
.onSuccess(
post -> rc.response().end(Json.encode(post))
)
.onFailure(
throwable -> rc.fail(throwable)
);
}
In the onFailure hook, use RoutingContext.fail to transit the exception state in route.
Let’s review the router definition in the /posts/:id route.
router.get("/posts/:id")
.produces("application/json")
.handler(handlers::get)
.failureHandler(frc -> {
Throwable failure = frc.failure();
if (failure instanceof PostNotFoundException) {
frc.response().setStatusCode(404).end();
}
frc.response().setStatusCode(500).setStatusMessage("Server internal error:" + failure.getMessage()).end();
});
There is a failure handler to handle exceptions in details.
In the above PostsHandler example, there is a fail alternative method accepts a status code parameter. If there is there is no failure handler in the router definition, it will send the the code as HTTP Status code to the client response.
Check the source codes from my Github.
For those cases which include a request body, such as create a new post , the request body should be validated to ensure it satisfies the requirements.
The validation progress can be done by a validation handler, similar to other request handlers, you can chain the handlers in your router definition.
router.post("/posts").consumes("application/json")
.handler(BodyHandler.create())
.handler(validation)
.handler(handlers::save)
.failureHandler(validationFailureHandler);
The BodyHandler is used to deserialize the request body, then validate the decoded body via a validation handler. If the validation is successful, call handlers::save to save the post data. A failure handler is declared in the last position to handle the possible validation exception thrown in the execution.
Vertx supports rich validation rule definitions based on Json Schema specification.
Add the following dependency into your pom.xml.
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-json-schema</artifactId>
</dependency>
The following is an example of defining a validation handler to validate the request body of creating a new post.
SchemaRouter schemaRouter = SchemaRouter.create(vertx, new SchemaRouterOptions());
SchemaParser schemaParser = SchemaParser.createDraft201909SchemaParser(schemaRouter);
ObjectSchemaBuilder bodySchemaBuilder = objectSchema()
.requiredProperty("title", stringSchema().with(minLength(5)).with(maxLength(100)))
.requiredProperty("content", stringSchema().with(minLength(10)).with(maxLength(2000)));
ValidationHandler validation = ValidationHandler.newInstance(
ValidationHandler
.builder(schemaParser)
//.queryParameter(param("parameterName", intSchema()))
//.pathParameter(param("pathParam", numberSchema()))
.body(Bodies.json(bodySchemaBuilder))
//.body(Bodies.formUrlEncoded(bodySchemaBuilder))
.predicate(RequestPredicate.BODY_REQUIRED)
.build()
);
The above is an example of using RxJava 3 Validation binding APIs, but there is an issue, you have to wrap the instance to create a RxJava3 specific validation handler.
When request body is failed to validate, it will throw a BodyProcessorException. The failure handler is used to handle the exception and send desired status to the response.
Handler<RoutingContext> validationFailureHandler = (RoutingContext rc) -> {
if (rc.failure() instanceof BodyProcessorException exception) {
rc.response()
.setStatusCode(400)
.end("validation failed.");
//.end(exception.toJson().encode());
}
};
Check the source codes from my Github.