1. Introduction
The HTTP protocol and APIs built on it are of central importance in programming these days.
On the JVM we have several available options, from lower-level to very high-level libraries, from established projects to new kids on the block. 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.
Before jumping into using this library, we must take into consideration the fact that it is no longer actively developed and maintained by the original author. Nonetheless, the library’s API documentation is up to date and may be used as a reference.
2. Dependencies
In order to use the library in our project, first, we have to add it to our dependencies. Even though the original artifact is no longer available, it was republished to Maven Central by the community:
<dependency>
<groupId>org.danilopianini</groupId>
<artifactId>khttp</artifactId>
<version>1.3.1</version>
</dependency>
3. Basic Usage
The basics of the HTTP protocol are simple, even though the fine details can be quite complicated. Therefore, khttp has a simple interface as well.
For every HTTP method, we can find a package-level function in the khttp package, such as get, post and so on.
The functions all take the same set of arguments and return a Response object; we’ll see the details of these in the following sections.
In the course of this article, we’ll use the fully qualified form, for example, khttp.put. In our projects we can, of course, import and possibly rename those methods:
import khttp.delete as httpDelete
Note: we’ve added type declarations for clarity throughout code examples because without an IDE they could be hard to follow.
4. A Simple Request
Every HTTP request has at least two required components: a method and a URL. In khttp, the method is determined by the function we invoke, as we’ve seen in the previous section.
The URL is the only required argument for the method; so, we can easily perform a simple request:
khttp.get("http://httpbin.org/get")
In the following sections, we’ll consider all requests to complete successfully.
4.1. Adding Parameters
We often have to provide query parameters in addition to the base URL, especially for GET requests.
khttp’s methods accept a params argument which is a Map of key-value pairs to include in the query String:
khttp.get(
url = "http://httpbin.org/get",
params = mapOf("key1" to "value1", "keyn" to "valuen"))
Notice that we’ve used the mapOf function to construct a Map on the fly; the resulting request URL will be:
http://httpbin.org/get?key1=value1&keyn=valuen
5. A Request Body
Another common operation we often need to perform is sending data, typically as the payload of a POST or PUT request.
For this, the library offers several options that we’re going to examine in the following sections.
5.1. Sending a JSON Payload
We can use the json argument to send a JSON object or array. It can be of several different types:
- A JSONObject or JSONArray as provided by the org.json library
- A Map, which is transformed into a JSON object
- A Collection, Iterable or array, which is transformed to a JSON array
We can easily turn our earlier GET example into a POST one which will send a simple JSON object:
khttp.post(
url = "http://httpbin.org/post",
json = mapOf("key1" to "value1", "keyn" to "valuen"))
Note that the transformation from collections to JSON objects is shallow. For example, a List of Map‘s won’t be converted to a JSON array of JSON objects, but rather to an array of strings.
For deep conversion, we’d need a more complex JSON mapping library such as Jackson. The conversion facility of the library is only meant for simple cases.
5.2. Sending Form Data (URL Encoded)
To send form data (URL encoded, as in HTML forms) we use the data argument with a Map:
khttp.post(
url = "http://httpbin.org/post",
data = mapOf("key1" to "value1", "keyn" to "valuen"))
5.3. Uploading Files (Multipart Form)
We can send one or more files encoded as a multipart form-data request.
In that case, we use the files argument:
khttp.post(
url = "http://httpbin.org/post",
files = listOf(
FileLike("file1", "content1"),
FileLike("file2", File("kitty.jpg"))))
We can see that khttp uses a FileLike abstraction, which is an object with a name and a content. The content can be a string, a byte array, a File, or a Path.
5.4. Sending Raw Content
If none of the options above are suitable, we can use an InputStream to send raw data as the body of an HTTP request:
khttp.post(url = "http://httpbin.org/post", data = someInputStream)
In this case, we’ll most likely need to manually set some headers too, which we’ll cover in a later section.
6. Handling the Response
So far we’ve seen various ways of sending data to a server. But many HTTP operations are useful because of the data they return as well.
khttp is based on blocking I/O, therefore all functions corresponding to HTTP methods return a Response object containing the response received from the server.
This object has various properties that we can access, depending on the type of content.
6.1. JSON Responses
If we know the response to be a JSON object or array, we can use the jsonObject and jsonArray properties:
val response : Response = khttp.get("http://httpbin.org/get")
val obj : JSONObject = response.jsonObject
print(obj["someProperty"])
6.2. Text or Binary Responses
If we want to read the response as a String instead, we can use the text property:
val message : String = response.text
Or, if we want to read it as binary data (e.g. a file download) we use the content property:
val imageData : ByteArray = response.content
Finally, we can also access the underlying InputStream:
val inputStream : InputStream = response.raw
7. Advanced Usage
Let’s also take a look at a couple of more advanced usage patterns that are generally useful, and that we haven’t yet treated in the previous sections.
7.1. Handling Headers and Cookies
All khttp functions take a headers argument which is a Map of header names and values.
val response = khttp.get(
url = "http://httpbin.org/get",
headers = mapOf("header1" to "1", "header2" to "2"))
Similarly for cookies:
val response = khttp.get(
url = "http://httpbin.org/get",
cookies = mapOf("cookie1" to "1", "cookie2" to "2"))
We can also access headers and cookies sent by the server in the response:
val contentType : String = response.headers["Content-Type"]
val sessionID : String = response.cookies["JSESSIONID"]
7.2. Handling Errors
There are two types of errors that can arise in HTTP: error responses, such as 404 – Not Found, which are part of the protocol; and low-level errors, such as “connection refused”.
The first kind doesn’t result in khttp throwing exceptions; instead, we should check the Response statusCode property:
val response = khttp.get(url = "http://httpbin.org/nothing/to/see/here")
if(response.statusCode == 200) {
process(response)
} else {
handleError(response)
}
Lower-level errors, instead, result in exceptions being thrown from the underlying Java I/O subsystem, such as ConnectException.
7.3. Streaming Responses
Sometimes the server can respond with a big piece of content, and/or take a long time to respond. In those cases, we may want to process the response in chunks, rather than waiting for it to complete and take up memory.
If we want to instruct the library to give us a streaming response, then we have to pass true as the stream argument:
val response = khttp.get(url = "http://httpbin.org", stream = true)
Then, we can process it in chunks:
response.contentIterator(chunkSize = 1024).forEach { arr : ByteArray -> handleChunk(arr) }
7.4. Non-Standard Methods
In the unlikely case that we need to use an HTTP method (or verb) that khttp doesn’t provide natively – say, for some extension of the HTTP protocol, like WebDAV – we’re still covered.
In fact, all functions in the khttp package, which correspond to HTTP methods, are implemented using a generic request function that we can use too:
khttp.request(
method = "COPY",
url = "http://httpbin.org/get",
headers = mapOf("Destination" to "/copy-of-get"))
7.5. Other Features
We haven’t touched all the features of khttp. For example, we haven’t discussed timeouts, redirects and history, or asynchronous operations.
The official documentation is the ultimate source of information about the library and all of its features.
8. Conclusion
In this tutorial, we’ve seen how to make HTTP requests in Kotlin with the idiomatic library khttp.
The implementation of all these examples can be found in the GitHub project.