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

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

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

Due to the --features kotlin flag, it generates a Kotlin 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 Groovy.

If you are using Java or Kotlin and IntelliJ IDEA make sure you have enabled annotation processing.

annotationprocessorsintellij

Kotlin, Kapt and IntelliJ

As of this writing IntelliJ’s built-in compiler does not directly support Kapt and annotation processing. You must instead configure Intellij to run Gradle (or Maven) compilation as a build step before running your tests or application class.

First edit the run configuration for tests or for the application and select "Run Gradle task" as a build step:

Intellij Settings

Then add the classes task as task to execute for the application or for tests the testClasses task:

Intellij Settings

Now whenever you run tests or the application Micronaut classes will be generated at compilation time.

Read Micronaut Kotlin section to learn more.

Alternatively, you can delegate IntelliJ build/run actions to gradle completely:

delegatetogradle

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/kotlin/example/micronaut/BintrayConfiguration.kt
package example.micronaut

import example.micronaut.BintrayConfiguration.Companion.PREFIX
import io.micronaut.context.annotation.ConfigurationProperties

@ConfigurationProperties(PREFIX)
class BintrayConfiguration {
    companion object {
        const val PREFIX = "bintray"
        const val BINTRAY_URL = "https://bintray.com"
    }

    var apiversion: String? = null
    var organization: String? = null
    var repository: String? = null
    var username: String? = null
    var token: String? = null

    fun toMap(): MutableMap<String, Any> {
        val m = HashMap<String, Any>()
        if (apiversion != null) {
            m["apiversion"] = apiversion!!
        }
        if (organization != null) {
            m["organization"] = organization!!
        }
        if (repository != null) {
            m["repository"] = repository!!
        }
        if (username != null) {
            m["username"] = username!!
        }
        if (token != null) {
            m["token"] = token!!
        }
        return m
    }
}

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 data class to parse the JSON response into it.

src/main/kotlin/example/micronaut/BintrayPackage.kt
package example.micronaut

data class BintrayPackage(var name: String = "",
                          var linked: Boolean = false) {
}

Create BintrayLowLevelClient:

src/main/kotlin/example/micronaut/BintrayLowLevelClient.kt
package example.micronaut

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.Maybe
import javax.inject.Singleton

@Singleton (1)
class BintrayLowLevelClient(@param:Client(BintrayConfiguration.BINTRAY_URL) private val httpClient: RxHttpClient, (2)
                            bintrayConfiguration: BintrayConfiguration) { (3)
    private val uri: String

    init {
        val path = "/api/{apiversion}/repos/{organization}/{repository}/packages"
        uri = UriTemplate.of(path).expand(bintrayConfiguration.toMap())
    }

    internal fun fetchPackages(): Maybe<List<BintrayPackage>> {
        val req = HttpRequest.GET<Any>(uri)  (4)
        val flowable = httpClient.retrieve(req, Argument.of(List::class.java, BintrayPackage::class.java))  (5)
        return 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/kotlin/example/micronaut/BintrayClient.kt
package example.micronaut

import io.micronaut.http.client.Client
import io.reactivex.Flowable
import io.micronaut.http.annotation.Get

@Client(BintrayConfiguration.BINTRAY_URL) (1)
interface BintrayClient {

    @Get("/api/\${bintray.apiversion}/repos/\${bintray.organization}/\${bintray.repository}/packages")  (2)
    fun fetchPackages(): Flowable<BintrayPackage>  (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/kotlin/example/micronaut/BintrayController.kt
package example.micronaut

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

@Controller("/bintray") (1)
class BintrayController(private val bintrayLowLevelClient : BintrayLowLevelClient,  (2)
                        private val bintrayClient: BintrayClient) {

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

    @Get(uri = "/packages", produces = arrayOf(MediaType.APPLICATION_JSON_STREAM))  (5)
    internal fun packages(): Flowable<BintrayPackage> {  (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

In this tutorial, we use Spek 2 to test the microservices:

gradle.properties
micronautVersion=1.0.0.M4
kotlinVersion=1.2.61
spekVersion=2.0.0-alpha.1
bookcatalogue/build.gradle
repositories {
...
..
    maven { url "https://dl.bintray.com/spekframework/spek-dev" }
}
dependencies {
    ...
    ..
    .
    testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlinVersion"
    testImplementation "org.spekframework.spek2:spek-dsl-jvm:$spekVersion"
    testRuntimeOnly "org.spekframework.spek2:spek-runner-junit5:$spekVersion"
}

test {
    useJUnitPlatform {
        includeEngines 'spek2'
    }
}

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

src/test/kotlin/example/micronaut/BintrayControllerSpec.kt
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.RxStreamingHttpClient
import io.micronaut.runtime.server.EmbeddedServer
import io.reactivex.Flowable
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

class BintrayControllerSpec: Spek({
    val expectedProfileNames = arrayOf("base", "federation", "function", "function-aws", "service")
    describe("BintrayController") {

        var embeddedServer : EmbeddedServer = ApplicationContext.run(EmbeddedServer::class.java) (1)
        var client : RxStreamingHttpClient = RxStreamingHttpClient.create(embeddedServer.url) (2)
        it("Verify bintray packages can be fetched with low level HttpClient") {
            val request = HttpRequest.GET<Any>("/bintray/packages-lowlevel")
            val rsp = client.toBlocking().exchange(request,   (3)
                    Argument.of(List::class.java, BintrayPackage::class.java))  (4)

            assertEquals(rsp.status()!!, HttpStatus.OK) (5)
            assertNotNull(rsp.body()) (6)

            for (name in expectedProfileNames) {
                var found = false
                for ( bintraypackage in rsp.body() as List<BintrayPackage>) {
                    found = name == bintraypackage.name
                    if(found) {
                        break
                    }
                }
                assertTrue(found)
            }
        }
        it("Verify bintray packages can be fetched with compile-time autogenerated @Client") {
            val request = HttpRequest.GET<Any>("/bintray/packages")
            val bintrayPackageStream : Flowable<BintrayPackage> = client.jsonStream(request, BintrayPackage::class.java)  (7)
            val bintrayPackages : Iterable<BintrayPackage> = bintrayPackageStream.blockingIterable()

            for (name in expectedProfileNames) {
                var found = false
                for ( bintraypackage in bintrayPackages) {
                    found = name == bintraypackage.name
                    if(found) {
                        break
                    }
                }
                assertTrue(found)
            }
        }

        afterGroup {
            client.close()
            embeddedServer.close()
        }

    }
})
1 To run the application from a unit test you can use the EmbeddedServer interface
2 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.
3 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.
4 Micronaut makes it easy to parse JSON into Kotlin objects.
5 Use status to check the HTTP status code.
6 Use .body() to retrieve the parsed payload.
7 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/kotlin/example/micronaut/BintrayFilter.kt
package example.micronaut

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

@Filter("/api/\${bintray.apiversion}/repos/**") (1)
@Requires(condition = BintrayFilterCondition::class)
class BintrayFilter(var bintrayConfiguration: BintrayConfiguration) : HttpClientFilter {

    override fun doFilter(request: MutableHttpRequest<*>, chain: ClientFilterChain): Publisher<out HttpResponse<*>> {
        if ( bintrayConfiguration.username != null && bintrayConfiguration.token != null) {
            return chain.proceed(request.basicAuth(bintrayConfiguration.username, bintrayConfiguration.token)) (4)
        }
        return return chain.proceed(request)

    }
}
1 Supply the pattern you want to match to the @Filter annotation. Note the required escaping (backslash) on "/api/\${bintray.apiversion}/repos/**" which is required due to Kotlin string interpolation.
2 Bean will not loaded unless condition is met
3 Constructor injection of the configuration parameters.
4 Enhance every request sent to Bintray API providing Basic Authentication.

We want to load the BintrayFilter bean if both configuration parameters bintray.username and bintray.token are present. In the Java or Groovy versions of this guide, multiple Requires annotations are used. Multiple @Requires annotations are not possible with Kotlin. Instead, we use a condition class.

src/main/kotlin/example/micronaut/BintrayFilterCondition.kt
package example.micronaut

import io.micronaut.context.condition.Condition
import io.micronaut.context.condition.ConditionContext
import io.micronaut.context.exceptions.NoSuchBeanException

class BintrayFilterCondition : Condition {

    override fun matches(context: ConditionContext<*>?): Boolean {
        if (context != null && context.beanContext != null) {
            try {
                val bintrayConfiguration: BintrayConfiguration = context.beanContext.getBean(BintrayConfiguration::class.java)
                if (bintrayConfiguration.token != null && bintrayConfiguration.username != null) {
                    return true
                }
            } catch (e: NoSuchBeanException) {
            }
        }
        return false
    }

}

We verify the previous condition class with a test:

src/test/kotlin/example/micronaut/BintrayFilterConditionSpec.kt
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.context.exceptions.NoSuchBeanException
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

class BintrayFilterConditionSpec: Spek({

    describe("BintrayFilter is loaded") {
        val applicationContext = ApplicationContext.run(mapOf<String, Any>(Pair("bintray.username", "john"), Pair("bintray.token", "XXX")), "test")
         it("Verify BintrayFilter is loaded if bintray.username/bintray.token are set") {
             assertNotNull(applicationContext.getBean(BintrayFilter::class.java))
        }
        afterGroup {
            applicationContext.close()
        }
    }

    describe("BintrayFilter is not loaded") {
        val applicationContext = ApplicationContext.run("test")
        it("Verify BintrayFilter is NOT loaded if bintray.username and bintray.token are not set") {
            var exceptionThrown = false
            try {
                applicationContext.getBean(BintrayFilter::class.java)
            } catch (e: NoSuchBeanException) {
                exceptionThrown = true
            }
            assertTrue(exceptionThrown)
        }
        afterGroup {
            applicationContext.close()
        }
    }
})

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?