Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

The REST client: calling other services

Services rarely live alone: a blog may need an author profile from a users service, an exchange rate from a bank, an address from a maps service. Writing HTTP requests by hand takes long. The Quarkus REST client works the other way round compared with a resource: you describe the other API as a Java interface with the same @Path and @GET annotations, and Quarkus creates the implementation that sends the requests and turns JSON into objects.

Adding it

./mvnw quarkus:add-extension -Dextensions="rest-client-jackson"

As the "users service" we use the public learning API jsonplaceholder.typicode.com. Its /users/1 address returns JSON with the id, name, email fields and many more.

The client interface

// src/main/java/com/example/blog/User.java
package com.example.blog;
 
public record User(long id, String name, String email) {
}
// src/main/java/com/example/blog/UserClient.java
package com.example.blog;
 
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
 
@Path("/users")
@RegisterRestClient(configKey = "users-api")
public interface UserClient {
 
    @GET
    @Path("/{id}")
    User getById(@PathParam("id") long id);
}

@RegisterRestClient tells Quarkus to create an implementation of the interface. configKey is the name under which the client looks up its settings. Jackson skips the extra fields of the response; only id, name and email go into the record.

The service address in the settings

# src/main/resources/application.properties
quarkus.rest-client.users-api.url=https://jsonplaceholder.typicode.com
quarkus.rest-client.users-api.connect-timeout=2000
quarkus.rest-client.users-api.read-timeout=5000

The address is not hard-coded, so on a server it can be changed with an environment variable. Timeouts in milliseconds keep a request from hanging forever if the service does not answer.

Using the client

The client is injected with the extra @RestClient qualifier:

// src/main/java/com/example/blog/AuthorResource.java
package com.example.blog;
 
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import org.eclipse.microprofile.rest.client.inject.RestClient;
 
@Path("/authors")
public class AuthorResource {
 
    @Inject
    @RestClient
    UserClient userClient;
 
    @GET
    @Path("/{id}")
    public User get(@PathParam("id") long id) {
        return userClient.getById(id);
    }
}
curl http://localhost:8080/authors/1
{"id":1,"name":"Leanne Graham","email":"Sincere@april.biz"}

Our service called the other API and returned a short author profile.

Errors of the remote service

If the remote service answers 404, the client throws a ClientWebApplicationException. To turn such a response into your own exception, add a static method with @ClientExceptionMapper to the interface:

// src/main/java/com/example/blog/UserClient.java
@ClientExceptionMapper
static RuntimeException toException(Response response) {
    if (response.getStatus() == 404) {
        return new AuthorNotFoundException();
    }
    return null;
}

The imports are io.quarkus.rest.client.reactive.ClientExceptionMapper and jakarta.ws.rs.core.Response. null means "handle as usual". The exception itself and its server mapper are written just like PostNotFoundException:

// src/main/java/com/example/blog/AuthorNotFoundException.java
package com.example.blog;
 
public class AuthorNotFoundException extends RuntimeException {
 
    public AuthorNotFoundException() {
        super("Author not found");
    }
}
// src/main/java/com/example/blog/ExceptionMappers.java
@ServerExceptionMapper
public RestResponse<ErrorMessage> mapAuthorNotFound(AuthorNotFoundException e) {
    return RestResponse.status(Response.Status.NOT_FOUND, new ErrorMessage(e.getMessage()));
}

Now curl -i http://localhost:8080/authors/999 returns 404 and {"error":"Author not found"}.

Common mistake

Without @ClientExceptionMapper an error of the other service becomes a 500 on our side:

HTTP/1.1 500 Internal Server Error

And the log says:

ERROR [io.quarkus.vertx.http.runtime.QuarkusErrorHandler] HTTP Request to /authors/999 failed, error id: ...:
org.jboss.resteasy.reactive.ClientWebApplicationException: Received: 'Not Found, status code 404'
when invoking REST Client method: 'com.example.blog.UserClient#getById'

Quarkus deliberately does not pass the remote status to the client as is: someone else's 404 does not mean that an address of our API was not found. Decide yourself what to answer, with @ClientExceptionMapper or a try/catch around the call.

Thanks for reading!

Donate to the site — we work hard for you.