Using the REST Client Reactive

This guide explains how to use the REST Client Reactive in order to interact with REST APIs. REST Client Reactive is a non-blocking counterpart of the RESTEasy REST Client.

If your application uses a client and exposes REST endpoints, please use RESTEasy Reactive for the server part.

Prerequisites

To complete this guide, you need:

  • less than 15 minutes

  • an IDE

  • JDK 11+ installed with JAVA_HOME configured appropriately

  • Apache Maven 3.8.1+

Solution

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

Clone the Git repository: git clone https://github.com/quarkusio/quarkus-quickstarts.git, or download an archive.

The solution is located in the rest-client-reactive-quickstart directory.

Creating the Maven project

First, we need a new project. Create a new project with the following command:

mvn io.quarkus.platform:quarkus-maven-plugin:2.6.3.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=rest-client-reactive-quickstart \
    -DclassName="org.acme.rest.client.ExtensionsResource" \
    -Dpath="/extension" \
    -Dextensions="resteasy-reactive-jackson,rest-client-reactive-jackson"
cd rest-client-reactive-quickstart

This command generates the Maven project with a REST endpoint and imports:

  • the resteasy-reactive-jackson extension for the REST server support. Use resteasy-reactive instead if you do not wish to use Jackson;

  • the rest-client-reactive-jackson extension for the REST client support. Use rest-client-reactive instead if you do not wish to use Jackson

If you already have your Quarkus project configured, you can add the rest-client-reactive-jackson extension to your project by running the following command in your project base directory:

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

This will add the following to your pom.xml:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-client-reactive-jackson</artifactId>
</dependency>

Setting up the model

In this guide we will be demonstrating how to consume part of the REST API supplied by the stage.code.quarkus.io service. Our first order of business is to set up the model we will be using, in the form of a Extension POJO.

Create a src/main/java/org/acme/rest/client/Extension.java file and set the following content:

package org.acme.rest.client;

import java.util.List;

public class Extension {

    public String id;
    public String name;
    public String shortName;
    public List<String> keywords;

}

The model above is only a subset of the fields provided by the service, but it suffices for the purposes of this guide.

Create the interface

Using the REST Client Reactive is as simple as creating an interface using the proper JAX-RS and MicroProfile annotations. In our case the interface should be created at src/main/java/org/acme/rest/client/ExtensionsService.java and have the following content:

package org.acme.rest.client;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import java.util.Set;

@Path("/extensions")
@RegisterRestClient
public interface ExtensionsService {

    @GET
    Set<Extension> getById(@QueryParam("id") String id);
}

The getById method gives our code the ability to get an extension by id from the Code Quarkus API. The client will handle all the networking and marshalling leaving our code clean of such technical details.

The purpose of the annotations in the code above is the following:

  • @RegisterRestClient allows Quarkus to know that this interface is meant to be available for CDI injection as a REST Client

  • @Path, @GET and @PathParam are the standard JAX-RS annotations used to define how to access the service

When the quarkus-rest-client-reactive-jackson extension is installed, Quarkus will use the application/json media type by default for most return values, unless the media type is explicitly set via @Produces or @Consumes annotations.

If you don’t rely on the JSON default, it is heavily recommended to annotate your endpoints with the @Produces and @Consumes annotations to define precisely the expected content-types. It will allow to narrow down the number of JAX-RS providers (which can be seen as converters) included in the native executable.

The getById method above is a blocking call. It should not be invoked on the event loop. The Async Support section describes how to make non-blocking calls.

Path Parameters

If the GET request requires path parameters you can leverage the @PathParam("parameter-name") annotation instead of (or in addition to) the @QueryParam. Path and query parameters can be combined, as required, as illustrated in the example below.

package org.acme.rest.client;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import java.util.Set;

@Path("/extensions")
@RegisterRestClient
public interface ExtensionsService {

    @GET
    @Path("/stream/{stream}")
    Set<Extension> getByStream(@PathParam("stream") String stream, @QueryParam("id") String id);
}

Create the configuration

In order to determine the base URL to which REST calls will be made, the REST Client uses configuration from application.properties. The name of the property needs to follow a certain convention which is best displayed in the following code:

# Your configuration properties
quarkus.rest-client."org.acme.rest.client.ExtensionsService".url=https://stage.code.quarkus.io/api # (1)
1 Having this configuration means that all requests performed using org.acme.rest.client.ExtensionsService will use https://stage.code.quarkus.io/api as the base URL. Using the configuration above, calling the getById method of ExtensionsService with a value of io.quarkus:quarkus-rest-client-reactive would result in an HTTP GET request being made to https://stage.code.quarkus.io/api/extensions?id=io.quarkus:quarkus-rest-client-reactive.

Note that org.acme.rest.client.ExtensionsService must match the fully qualified name of the ExtensionsService interface we created in the previous section.

To facilitate the configuration, you can use the @RegisterRestClient configKey property that allows to use different configuration root than the fully qualified name of your interface.

@RegisterRestClient(configKey="extensions-api")
public interface ExtensionsService {
    [...]
}
# Your configuration properties
quarkus.rest-client.extensions-api.url=https://stage.code.quarkus.io/api
quarkus.rest-client.extensions-api.scope=javax.inject.Singleton

Update the JAX-RS resource

Open the src/main/java/org/acme/rest/client/ExtensionsResource.java file and update it with the following content:

package org.acme.rest.client;

import io.smallrye.common.annotation.Blocking;
import org.eclipse.microprofile.rest.client.inject.RestClient;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import java.util.Set;

@Path("/extension")
public class ExtensionsResource {

    @RestClient (1)
    ExtensionsService extensionsService;


    @GET
    @Path("/id/{id}")
    @Blocking (2)
    public Set<Extension> id(String id) {
        return extensionsService.getById(id);
    }
}

There are two interesting parts in this listing:

1 the client stub is injected with the @RestClient annotation instead of the usual CDI @Inject
2 the call we are making with the client is blocking, hence we need the @Blocking annotation on the REST endpoint

Programmatic client creation with RestClientBuilder

Instead of annotating the client with @RegisterRestClient, and injecting a client with @RestClient, you can also create REST Client programmatically. You do that with RestClientBuilder.

With this approach the client interface could look as follows:

package org.acme.rest.client;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import java.util.Set;

@Path("/extensions")
public interface ExtensionsService {

    @GET
    Set<Extension> getById(@QueryParam("id") String id);
}

And the service as follows:

package org.acme.rest.client;

import io.smallrye.mutiny.Uni;
import org.eclipse.microprofile.rest.client.RestClientBuilder;
import org.eclipse.microprofile.rest.client.inject.RestClient;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import java.util.Set;
import java.util.concurrent.CompletionStage;

@Path("/extension")
public class ExtensionsResource {

    private final ExtensionsService extensionsService;

    public ExtensionsResource() {
        extensionsService = RestClientBuilder.newBuilder()
            .baseUri("https://stage.code.quarkus.io/api")
            .build(ExtensionsService.class);
    }

    @GET
    @Path("/id/{id}")
    public Set<Extension> id(String id) {
        return extensionsService.getById(id);
    }
}

Update the test

Next, we need to update the functional test to reflect the changes made to the endpoint. Edit the src/test/java/org/acme/rest/client/ExtensionsResourceTest.java file and change the content of the test to:

package org.acme.rest.client;

import io.quarkus.test.junit.QuarkusTest;

import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.greaterThan;

@QuarkusTest
public class ExtensionsResourceTest {

    @Test
    public void testExtensionsIdEndpoint() {
        given()
            .when().get("/extension/id/io.quarkus:quarkus-rest-client-reactive")
            .then()
            .statusCode(200)
            .body("$.size()", is(1),
                "[0].id", is("io.quarkus:quarkus-rest-client-reactive"),
                "[0].name", is("REST Client Reactive"),
                "[0].keywords.size()", greaterThan(1),
                "[0].keywords", hasItem("rest-client"));
    }
}

The code above uses REST Assured's json-path capabilities.

Async Support

To get the full power of the reactive nature of the client, you can use the non-blocking flavor of REST Client Reactive extension, which comes with support for CompletionStage and Uni. Let’s see it in action by adding a getByIdAsync method in our ExtensionsService REST interface. The code should look like:

package org.acme.rest.client;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import java.util.Set;
import java.util.concurrent.CompletionStage;

@Path("/extensions")
@RegisterRestClient(configKey = "extensions-api")
public interface ExtensionsService {

    @GET
    Set<Extension> getById(@QueryParam("id") String id);

    @GET
    CompletionStage<Set<Extension>> getByIdAsync(@QueryParam("id") String id);
}

Open the src/main/java/org/acme/rest/client/ExtensionsResource.java file and update it with the following content:

package org.acme.rest.client;

import io.smallrye.common.annotation.Blocking;
import org.eclipse.microprofile.rest.client.inject.RestClient;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import java.util.Set;
import java.util.concurrent.CompletionStage;

@Path("/extension")
public class ExtensionsResource {

    @RestClient
    ExtensionsService extensionsService;


    @GET
    @Path("/id/{id}")
    @Blocking
    public Set<Extension> id(String id) {
        return extensionsService.getById(id);
    }

    @GET
    @Path("/id-async/{id}")
    public CompletionStage<Set<Extension>> idAsync(String id) {
        return extensionsService.getByIdAsync(id);
    }
}

Please note that since the invocation is now non-blocking, we don’t need the @Blocking annotation anymore on the endpoint. This means that the idAsync method will be invoked on the event loop, i.e. will not get offloaded to a worker pool thread and thus reducing hardware resource utilization.

To test asynchronous methods, add the test method below in ExtensionsResourceTest:

@Test
public void testExtensionIdAsyncEndpoint() {
    given()
        .when().get("/extension/id-async/io.quarkus:quarkus-rest-client-reactive")
        .then()
        .statusCode(200)
        .body("$.size()", is(1),
            "[0].id", is("io.quarkus:quarkus-rest-client-reactive"),
            "[0].name", is("REST Client Reactive"),
            "[0].keywords.size()", greaterThan(1),
            "[0].keywords", hasItem("rest-client"));
}

The Uni version is very similar:

package org.acme.rest.client;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.annotations.jaxrs.PathParam;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import java.util.Set;
import java.util.concurrent.CompletionStage;

@Path("/extensions")
@RegisterRestClient(configKey = "extensions-api")
public interface ExtensionsService {

    // ...

    @GET
    Uni<Set<Extension>> getByIdAsUni(@QueryParam("id") String id)
}

The ExtensionsResource becomes:

package org.acme.rest.client;

import io.smallrye.common.annotation.Blocking;
import io.smallrye.mutiny.Uni;
import org.eclipse.microprofile.rest.client.inject.RestClient;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import java.util.Set;
import java.util.concurrent.CompletionStage;

@Path("/extension")
public class ExtensionsResource

    @RestClient
    ExtensionsService extensionsService;


    // ...

    @GET
    @Path("/id-uni/{id}")
    public Uni<Set<Extension>> idUni(String id) {
        return extensionsService.getByIdAsUni(id);
    }
}
Mutiny

The previous snippet uses Mutiny reactive types. If you are not familiar with Mutiny, check Mutiny - an intuitive reactive programming library.

When returning a Uni, every subscription invokes the remote service. It means you can re-send the request by re-subscribing on the Uni, or use a retry as follows:

@RestClient ExtensionsService extensionsService;

// ...

extensionsService.getByIdAsUni(id)
    .onFailure().retry().atMost(10);

If you use a CompletionStage, you would need to call the service’s method to retry. This difference comes from the laziness aspect of Mutiny and its subscription protocol. More details about this can be found in the Mutiny documentation.

Custom headers support

There are a few ways in which you can specify custom headers for your REST calls:

  • by registering a ClientHeadersFactory or a ReactiveClientHeadersFactory with the @RegisterClientHeaders annotation

  • by specifying the value of the header with @ClientHeaderParam

  • by specifying the value of the header by @HeaderParam

The code below demonstrates how to use each of these techniques:

package org.acme.rest.client;

import io.smallrye.mutiny.Uni;
import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import java.util.Set;
import java.util.concurrent.CompletionStage;

@Path("/extensions")
@RegisterRestClient
@RegisterClientHeaders(RequestUUIDHeaderFactory.class) (1)
@ClientHeaderParam(name = "my-header", value = "constant-header-value") (2)
@ClientHeaderParam(name = "computed-header", value = "{org.acme.rest.client.Util.computeHeader}") (3)
public interface ExtensionsService {

    @GET
    @ClientHeaderParam(name = "header-from-properties", value = "${header.value}") (4)
    Set<Extension> getById(@QueryParam("id") String id, @HeaderParam("jaxrs-style-header") String headerValue); (5)
}
1 There can be only one ClientHeadersFactory per class. With it, you can not only add custom headers, but you can also transform existing ones. See the RequestUUIDHeaderFactory class below for an example of the factory.
2 @ClientHeaderParam can be used on the client interface and on methods. It can specify a constant header value…​
3 …​ and a name of a method that should compute the value of the header. It can either be a static method or a default method in this interface
4 …​ as well as a value from your application’s configuration
5 …​ or as a normal JAX-RS @HeaderParam annotated argument

A ClientHeadersFactory can look as follows:

package org.acme.rest.client;

import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;

import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import java.util.UUID;

@ApplicationScoped
public class RequestUUIDHeaderFactory implements ClientHeadersFactory {

    @Override
    public MultivaluedMap<String, String> update(MultivaluedMap<String, String> incomingHeaders, MultivaluedMap<String, String> clientOutgoingHeaders) {
        MultivaluedMap<String, String> result = new MultivaluedHashMap<>();
        result.add("X-request-uuid", UUID.randomUUID().toString());
        return result;
    }
}

As you see in the example above, you can make your ClientHeadersFactory implementation a CDI bean by annotating it with a scope-defining annotation, such as @Singleton, @ApplicationScoped, etc.

To specify a value for ${header.value}, simply put the following in your application.properties:

header.value=value of the header

Also, there is a reactive flavor of ClientHeadersFactory that allows doing blocking operations. For example:

package org.acme.rest.client;

import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;

import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import java.util.UUID;

@ApplicationScoped
public class GetTokenReactiveClientHeadersFactory extends ReactiveClientHeadersFactory {

    @Inject
    Service service;

    @Override
    public Uni<MultivaluedMap<String, String>> getHeaders(MultivaluedMap<String, String> incomingHeaders) {
        return Uni.createFrom().item(() -> {
            MultivaluedHashMap<String, String> newHeaders = new MultivaluedHashMap<>();
            // perform blocking call
            newHeaders.add(HEADER_NAME, service.getToken());
            return newHeaders;
        });
    }
}

Default header factory

The @RegisterClientHeaders annotation can also be used without any custom factory specified. In that case the DefaultClientHeadersFactoryImpl factory will be used. If you make a REST client call from a REST resource, this factory will propagate all the headers listed in org.eclipse.microprofile.rest.client.propagateHeaders configuration property from the resource request to the client request. Individual header names are comma-separated.

@Path("/extensions")
@RegisterRestClient
@RegisterClientHeaders
public interface ExtensionsService {

    @GET
    Set<Extension> getById(@QueryParam("id") String id);

    @GET
    CompletionStage<Set<Extension>> getByIdAsync(@QueryParam("id") String id);
}
org.eclipse.microprofile.rest.client.propagateHeaders=Authorization,Proxy-Authorization

Multipart Form support

Rest Client Reactive allows sending data as multipart forms. This way you can for example send files efficiently.

To send data as a multipart form, you need to create a class that would encapsulate all the fields to be sent, e.g.

class FormDto {
    @FormParam("file")
    @PartType(MediaType.APPLICATION_OCTET_STREAM)
    public File file;

    @FormParam("otherField")
    @PartType(MediaType.TEXT_PLAIN)
    public String textProperty;
}

The method that sends a form needs to specify multipart form data as the consumed media type, e.g.

    @POST
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/binary")
    String sendMultipart(@MultipartForm FormDto data);

Fields specified as File, Path, byte[] or Buffer are sent as files; as binary files for @PartType(MediaType.APPLICATION_OCTET_STREAM), as text files for other content types. Other fields are sent as form attributes.

There are a few modes in which the form data can be encoded. By default, Rest Client Reactive uses RFC1738. You can override it by specifying the mode either on the client level, by setting io.quarkus.rest.client.multipart-post-encoder-mode RestBuilder property to the selected value of HttpPostRequestEncoder.EncoderMode or by specifying quarkus.rest-client.multipart-post-encoder-mode in your application.properties. Please note that the latter works only for clients created with the @RegisterRestClient annotation. All the available modes are described in the Netty documentation

Package and run the application

Run the application with: ./mvnw compile quarkus:dev. Open your browser to http://localhost:8080/extension/id/io.quarkus:quarkus-rest-client-reactive.

You should see a JSON object containing some basic information about this extension.

As usual, the application can be packaged using ./mvnw clean package and executed using the target/quarkus-app/quarkus-run.jar file. You can also generate the native executable with ./mvnw clean package -Pnative.

Using a Mock HTTP Server for tests

For tests, you can easily mock the HTTP server with Wiremock. The Wiremock section of the Quarkus - Using the REST Client describes how to set it up in detail.

Known limitations

While the REST Client Reactive extension aims to be a drop-in replacement for the REST Client extension, there are some differences and limitations:

  • the default scope of the client for the new extension is @ApplicationScoped while the quarkus-rest-client defaults to @Dependent To change this behavior, set the quarkus.rest-client-reactive.scope property to the fully qualified scope name.

  • it is not possible to set HostnameVerifier or SSLContext

  • a few things that don’t make sense for a non-blocking implementations, such as setting the ExecutorService, don’t work