Friday 7 October 2022

Kotlin: getting the khttp library installed and running... then... getting rid of it and using something else

G'day:

So 50% of this was a largely fruitless exercise in the end. Other than the fact that I learned some stuff that I think is worth knowing for future reference.

The next thing on my Kotlin/testing list is a more practical exercise: I need to be able to write tests for web service endpoints. To do that I need to be able to make HTTP requests. I second-guessed there'd be really awful boilerplate-laiden Java ways of doing things, but I wanted a Kotlin-idiomatic way of doing it.


khttp

I googled "kotlin http requests", and one of the first links was to an article on the Baeldung website: "HTTP Requests with Kotlin and khttp". Their articles ae usually pretty solid, so I didn't look any further. It was especially appealing because they made a point of saying:

On the JVM we have several available options, from lower-level to very high-level libraries[…]. However, most of them are targeted primarily at Java programs.

In this article, we’re going to look at khttp, an idiomatic Kotlin library for consuming HTTP-based resources and APIs.

Perfect.

First I had to install this khttp thing. The instructions were for a hand-cranked Maven install, but I can't be arsed with that at the moment, and I wanna use Gradle via my build.gradle.kts file. Other things I've looked at have supplied the installation instructions for that, which is really handy for a n00b like me as I don't really know what I'm doing and what the various values mean in the Maven XML. And TBH: I don't want to know. But I figured I could work out from the XML:

<dependency>
    <groupId>khttp</groupId>
    <artifactId>khttp</artifactId>
    <version>0.1.0</version>
</dependency>

<repository>
    <id>central</id>
    <url>http://jcenter.bintray.com</url>
</repository>

I need the repository cos it's not on Maven Central yet apparently. Cool: I know that one can specify repositories and dependencies in the Gradle file, so how hard could it be.

repositories {
    mavenCentral()
    maven("https://jcenter.bintray.com")
}

dependencies {
    // …
    testImplementation("khttp:khttp:0.1.0")
}

All I needed to do is to change the repo's scheme from http to https, because with http I was getting an error:

Using insecure protocols with repositories, without explicit opt-in, is unsupported.

And that's fair enough. It installed OK over https.

I created my test:

@DisplayName("Tests of WebService class")
internal class WebServiceTest {

    val serviceUrl = "https://example.com/"
    @Test
    fun `it returns a 200`() {
        val response = khttp.get(serviceUrl)

        response.statusCode shouldBe 200
    }
}

And ran it:

Unable to make field private java.lang.String java.net.URL.host accessible: module java.base does not "opens java.net" to unnamed module @645aa696 java.lang.reflect.InaccessibleObjectException: Unable to make field private java.lang.String java.net.URL.host accessible: module java.base does not "opens java.net" to unnamed module @645aa696 at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)

Not what I wanted to see. Not least of all cos I have NFI what that means. I poss should have noticed the mention of reflection in which case I might have recalled that there's been warnings emiting from Java about this when I start-up Lucee:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by [x] to method [y]
WARNING: Please consider reporting this to the maintainers of [x]
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

Had I thought about it I would have landed on the fix sooner. It took me about an hour to find a comment on the khttp projects GitHub issues: ascclemens/khttp issue #88. I am running on a Java 16 JDK, and that reflective thing got blocked from that version on. The best answer on the issue was:

For enabling khttp 1.0 with JDK 17.0 I use these VM arguments:

--add-opens java.base/java.net=ALL.UNNAMED --add-opens java.base/sun.net.www.protocol.https=ALL-UNNAMED

Cool. I had a hunt around in the settings of IntelliJ a bit to find out where I could sling those. I found a few places that took VM arguments, which looked promising, but none worked when I rebuild the project. I also found a way of putting them in my build file:

application {
    mainClass.set("MainKt")
    listOf(
        "--add-opens java.base/java.net=ALL.UNNAMED",
        "--add-opens java.base/sun.net.www.protocol.https=ALL-UNNAMED"
    ).also { applicationDefaultJvmArgs = it }
}

Which is good to know, but also didn't work.

In the end I downloaded Java 15 JDK and used that instead. For future me, the setting that matter is the Gradle JVM:

It is not just a matter of changing the Java or Kotlin byte-code versions:

I could switch those to 15, and it didn't make a difference. But once I changed Gradles JVM to 15, it worked:

But do you know what? I wasn't so happy with reverting my JVM version to accommodate a library I was using for testing, so I decided to see if there was another one. And this is when I wished I had looked below the fold on the first page of Google results…


Ktor

Ktor is the framework we have decided to use for our project @ work, so it kinda makes sense to use its own tooling. I'm gonna have a look at their offering.

Installation was simple:

implementation("io.ktor:ktor-client-core:2.1.2")
implementation("io.ktor:ktor-client-cio:2.1.2")

The first one is self-explanatory; the second is the underlying engine the client uses it seems. There's some docs: Ktor Client / Developing applications / Setting up a client / Engines.

From there the test was a bit of horsing around:

fun `it returns a 200`() {
    runBlocking {
        HttpClient().use { client ->
            val response = client.get("https://jsonplaceholder.typicode.com/todos/1")
            response.status shouldBe HttpStatusCode.OK
        }
    }
}

The CIO engine is asynchronous, so all this is using coroutines which I don't have headspace for ATM, but it seems the "async await" approach to making async code run in non-async code is to sling a runBlocking block around it. Fine.

One cool Kotlin thing here is that I did have this code written like this:

fun `it returns a 200`() {
    runBlocking {
        val client = HttpClient()
        try {
            val response = client.get("https://jsonplaceholder.typicode.com/todos/1")
            response.status shouldBe HttpStatusCode.OK
        } finally {
            client.close()
        }
    }
}

IntelliJ had a squiggly line under the try, and the hint said "try-finally can be replaced with 'use()'". Um: sure. Can it? OK. Let's do that then. And it changed it for me to the code I initially showed.

I was bemused that the client.close() call had gone completely. I RTFMed a bit, and use is quite cool:

Executes the given block function on this resource and then closes it down correctly whether an exception is thrown or not.

It can be called on any object that implements the Closeable interface (although use is a Kotlin extension to this). If one looks at the implementation of Closeable.use we see what's going on (this is heavily elided):

fun use() {
    try {
        return block(this)
    } finally {
        try {
            close()
        } catch (closeException: Throwable) {
        }
    }
}

This use approach just makes sense.


Thoughts

In contrasting the very simple khttp approach to the Ktor approach, I prefer the khttp one. This reminds me of GuzzleHttp for PHP: it started off being a simple and useful and just… did HTTP requests. No async, no promises, it just got on with it. Then it decided async was all the rage so it took a very simple interface and made it overly complex for the task at hand (not to mention not backwards compatible). Ktor's approach seems similar. When I reflect upon how often I really need my HTTP requests to be done asynchronously to my mainline code, the answer is fucking never. Or so infrequently then I'm happy to write an async wrapper for the call if I need it.

But: we're already using Ktor, and it's more mature, and I don't have to retrograde to an old version of Java to use it. So Ktor and its unecessary complexity it is.

I'll try to do something more useful with this stuff tomorrow, but getting everything working (twice) took more time than I wanted to spend on this, so I'm stopping here for now.

The code for this is on GitHub @ /src/test/kotlin/junit/practical/WebServiceTest.kt.

Righto.

--
Adam