Micronaut HTTP Client

Learn how to use Micronaut low-level HTTP Client. Simplify your code with the declarative HTTP client.

Authors: Sergio del Amo

Micronaut Version: 1.0.0.M4

1 Getting Started

In this guide we are going to create a Micronaut app written in Groovy to consume the Bintray API with the Micronaut HTTP Client.

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 a Groovy Micronaut app using the Micronaut Command Line Interface.

mn create-app example.micronaut.complete --features=groovy

The previous command createas a micronaut app with the default package example.micronaut in a folder named complete.

Due to the --features groovy flag, it generates a Groovy Micronaut app and it uses Gradle build system. However, you could use other build tool such as Maven or other programming languages such as Java or Kotlin.

3 Bintray API

In this guide, you are going to consume the Bintray API from a Micronaut application.

In particular, we consume the Get Packages endpoint.

Get a list of packages in the specified repository, optionally specify a starting position and/or a name prefix filter.

This API resource can be consumed by both authenticated and anonymous clients.

Initially, you will consume it anonymously, later we will discuss authentication.

Modify src/main/resources/application.yml to create some configuration parameters.

src/main/resources/application.yml
bintray:
    organization: micronaut
    repository: profiles
    apiversion: v1

To encapsulate type-safe configuration retrieval, we use a @ConfigurationProperties object:

src/main/groovy/example/micronaut/BintrayConfiguration.groovy
package example.micronaut

import groovy.transform.CompileStatic
import io.micronaut.context.annotation.ConfigurationProperties
import io.micronaut.context.annotation.Requires

@CompileStatic
@ConfigurationProperties(BintrayConfiguration.PREFIX)
@Requires(property = BintrayConfiguration.PREFIX)
class BintrayConfiguration {

    public static final String PREFIX = "bintray"
    public static final String BINTRAY_API_URL = "https://bintray.com"

    String apiversion

    String organization

    String repository

    String username

    String token

    Map<String, Object> toMap() {
        [
                apiversion: apiversion,
                organization: organization,
                repository: repository,
                username: username,
                token: token
        ] as Map<String, Object>
    }
}

In this guide, you are going to fetch Micronaut profiles packages. Those packages are used by the Micronaut CLI.

To consume the Bintray API, you will use Micronaut HTTP Client.

4 Low Level Client

Initially, you will create a Bean which uses the low-level Client API.

Create a POJO to parse the JSON response into a Java object:

src/main/groovy/example/micronaut/BintrayPackage.groovy
package example.micronaut

import groovy.transform.CompileStatic

@CompileStatic
class BintrayPackage {
    String name
    boolean linked
}

Create BintrayLowLevelClient:

src/main/groovy/example/micronaut/BintrayLowLevelClient.groovy
package example.micronaut

import groovy.transform.CompileStatic
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.Client
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.uri.UriTemplate
import io.reactivex.Flowable
import io.reactivex.Maybe

import javax.inject.Singleton

@CompileStatic
@Singleton (1)
class BintrayLowLevelClient {
    private final RxHttpClient httpClient
    private final String uri

    BintrayLowLevelClient(@Client(BintrayConfiguration.BINTRAY_API_URL) RxHttpClient httpClient, (2)
                          BintrayConfiguration configuration) { (3)
        this.httpClient = httpClient
        String path = "/api/{apiversion}/repos/{organization}/{repository}/packages"
        uri = UriTemplate.of(path).expand(configuration.toMap())
    }

    Maybe<List<BintrayPackage>> fetchPackages() {
        HttpRequest<?> req = HttpRequest.GET(uri) (4)
        Flowable flowable = httpClient.retrieve(req, Argument.of(List, BintrayPackage)) (5)
        flowable.firstElement() as Maybe<List<BintrayPackage>> (6)
    }
}
1 Use javax.inject.Singleton to designate a class a a singleton.
2 Inject RxClient via constructor injection
3 Inject the previously defined configuration parameters.
4 Creating HTTP Requests is easy thanks to Micronaut’s fluid API.
5 Use retrieve to perform an HTTP request for the given request object and convert the full HTTP response’s body into the specified type. e.g. List<BintrayPackage>.
6 The retrieve method returns a Flowable which has a firstElement method that returns the first emitted item or nothing
Instead of retrieve we could have used jsonStream. You can use jsonStream() to stream arrays of type application/json or JSON streams of type application/x-json-stream. If we use retrieve, such as in the previous code listing, the operation will not block. However, it will not return until all the data has been received from the server. In the case of a JSON array that would be the whole array. However, if you are interested in just the first element of the array, jsonStream provides a better alternative since it starts streaming data from the server without needing the whole response. For example, jsonStream().firstElement() will only parse the first item in a JSON array. Hence it is more efficient.

5 Declarative Client

It is time to take a look at Micronaut’s support for declarative clients via the Client annotation.

Create BintrayClient which clearly illustrates how a declarative Micronaut HTTP Client, which is generated at compile-time, simplifies our code.

src/main/groovy/example/micronaut/BintrayClient.groovy
package example.micronaut

import groovy.transform.CompileStatic
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.Client
import io.reactivex.Flowable

@CompileStatic
@Client(BintrayConfiguration.BINTRAY_API_URL) (1)
interface BintrayClient {
    @Get('/api/${bintray.apiversion}/repos/${bintray.organization}/${bintray.repository}/packages') (2)
    Flowable<BintrayPackage> fetchPackages() (3)
}
1 URL of the remote service
2 You can use configuration parameter interpolation when you define the path of the GET endpoint.
3 You can return reactive types, such as an RxJava Flowable.

6 Controller

Create a Controller. It uses both (low-level and declarative clients). It showcases several Micronaut’s capabilities.

  • Micronaut supports any framework that implements Reactive Streams, including RxJava, and Reactor. Thus, you can easily and efficiently compose multiple HTTP client calls without blocking (which will limit the throughput and scalability of your application).

  • Micronaut enables you to consume/produce JSON Streams.

src/main/groovy/example/micronaut/BintrayController.groovy
package example.micronaut

import groovy.transform.CompileStatic
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.reactivex.Flowable
import io.reactivex.Maybe

@CompileStatic
@Controller("/bintray") (1)
class BintrayController {
    private final BintrayLowLevelClient bintrayLowLevelClient

    private final BintrayClient bintrayClient

    BintrayController(BintrayLowLevelClient bintrayLowLevelClient, (2)
                             BintrayClient bintrayClient) {
        this.bintrayLowLevelClient = bintrayLowLevelClient
        this.bintrayClient = bintrayClient
    }

    @Get("/packages-lowlevel") (3)
    Maybe<List<BintrayPackage>> packagesWithLowLevelClient() { (4)
        return bintrayLowLevelClient.fetchPackages()
    }

    @Get(uri = "/packages", produces = MediaType.APPLICATION_JSON_STREAM) (5)
    Flowable<BintrayPackage> packages() { (6)
        return bintrayClient.fetchPackages()
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /bintray
2 Inject beans via constructor injection.
3 The @Get annotation is used to map the index method to all requests that use an HTTP GET
4 The packagesWithLowLevelClient returns a Maybe which may or may not emit an item. If an item is not emitted a 404 is returned.
5 In order to do JSON streaming you can declare a controller method that returns a application/x-json-stream of JSON objects.
6 You can return reactive types, such as an RxJava Flowable.

7 Test

Create a Spock test which verifies both clients work as expected and the controller echoes the output of the Bintray API in a Reactive way.

src/test/groovy/example/micronaut/BintrayControllerSpec.groovy
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.RxStreamingHttpClient
import io.micronaut.runtime.server.EmbeddedServer
import io.reactivex.Flowable
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

class BintrayControllerSpec extends Specification {

    @Shared
    @AutoCleanup (1)
    EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) (2)

    @Shared
    @AutoCleanup
    RxStreamingHttpClient client = embeddedServer.applicationContext.createBean(RxStreamingHttpClient, embeddedServer.getURL()) (3)

    @Shared
    List<String> expectedProfileNames = ['base', 'federation', 'function', 'function-aws', 'service']

    def "Verify bintray packages can be fetched with low level HttpClient"() {
        when:
        HttpRequest request = HttpRequest.GET('/bintray/packages-lowlevel')

        HttpResponse<List<BintrayPackage>> rsp = client.toBlocking().exchange(request, (4)
                Argument.of(List, BintrayPackage)) (5)

        then: 'the endpoint can be accessed'
        rsp.status == HttpStatus.OK (6)
        rsp.body() (7)

        when:
        List<BintrayPackage> packages = rsp.body()

        then:
        for (String name : expectedProfileNames) {
            assert packages*.name.contains(name)
        }
    }

    def "Verify bintray packages can be fetched with compile-time autogenerated @Client"() {
        when:
        HttpRequest request = HttpRequest.GET('/bintray/packages')

        Flowable<BintrayPackage> bintrayPackageStream = client.jsonStream(request, BintrayPackage) (8)
        Iterable<BintrayPackage> bintrayPackages = bintrayPackageStream.blockingIterable()

        then:
        for (String name : expectedProfileNames) {
            assert bintrayPackages*.name.contains(name)
        }
    }
}
1 The AutoCleanup extension makes sure the close() method of an object (e.g. EmbeddedServer) is called each time a feature method is finished
2 To run the application from a unit test you can use the EmbeddedServer interface
3 Micronaut’s HTTP client includes support for streaming data over HTTP via the RxStreamingHttpClient. Register a RxStreamingHttpClient bean in the application context and point it to the embedded server URL. The EmbeddedServer interface provides the URL of the server under test which runs on a random port.
4 Sometimes, receiving just the object is not enough and you need information about the response. In this case, instead of retrieve you should use the exchange method.
5 Micronaut makes it easy to parse JSON into Java objects.
6 Use status to check the HTTP status code.
7 Use .body() to retrieve the parsed payload.
8 Use the jsonStream method, which returns a Flowable, to consume the endpoint which generates a JSON Stream.

8 Testing the Application

To run the tests:

$ ./gradlew test
$ open build/reports/tests/test/index.html

9 Http Client Filter

Often, you need to include the same HTTP headers or URL parameters in a set of requests against a third-party API or when calling another Microservice.

To simplify this, Micronaut includes the ability to define HttpClientFilter classes that are applied to all matching HTTP clients.

For real world example, let us provide Bintray Authentication via a HttpClientFilter:

The Bintray REST API requires an applicative API key. An API key can be obtained from the user profile page. Authentication is achieved using HTTP Basic Authentication with the user’s name as username and the API key as the password. Authenticated REST calls should only be used via HTTPs.

Create a Filter:

src/main/groovy/example/micronaut/BintrayFilter.groovy
package example.micronaut

import groovy.transform.CompileStatic
import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpResponse
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.annotation.Filter
import io.micronaut.http.filter.ClientFilterChain
import io.micronaut.http.filter.HttpClientFilter
import org.reactivestreams.Publisher

@CompileStatic
@Filter('/api/${bintray.apiversion}/repos/**') (1)
@Requires(property = "bintray.username") (2)
@Requires(property = "bintray.token") (2)
class BintrayFilter  implements HttpClientFilter {

    private final BintrayConfiguration configuration

    BintrayFilter(BintrayConfiguration configuration ) { (3)
        this.configuration = configuration
    }

    @Override
    Publisher<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request, ClientFilterChain chain) {
        return chain.proceed(request.basicAuth(configuration.username, configuration.token)) (4)
    }
}
1 Supply the pattern you want to match to the @Filter annotation.
2 Bean will not loaded unless configuration properties are set.
3 Constructor injection of the configuration parameters.
4 Enhance every request sent to Bintray API providing Basic Authentication.

Configuration Parameters

Add your Bintray username and token to src/main/resource/application.yml

bintray:
    organization: micronaut
    repository: profiles
    apiversion: v1
    username: yourbintrayusername
    token: XXXXXXXXXXX

Add to src/main/resources/logback.xml, a logger to see Micronaut’s HTTP client output.

<logger name="io.micronaut.http.client" level="TRACE"/>

If you run again the tests, you will see the that the Filter is invoked and HTTP Basic Auth is used against Bintray API.

13:21:08.981 [nioEventLoopGroup-1-14] TRACE i.m.http.client.DefaultHttpClient - Authorization: Basic XXXXXXXXXXXXXX

10 Where To Go From Here?