Skip to content

HTTP Client

Module http_client

This port provides a common interface for using HTTP clients. Many adapters can be developed to use different technologies.

Its main functionalities are:

  • HTTP, HTTPS and HTTP/2 support
  • Mutual TLS
  • Body encoding/decoding
  • Request/response exchange
  • Form submissions
  • Cookie management
  • File uploading/downloading

Install the Dependency

This module is not meant to be used directly. You should include an Adapter implementing this feature (as http_client_jetty) in order to create HTTP clients.

Create an HTTP client

You create an HTTP Client instance with default options as follows:

1
2
HttpClient(adapter)
HttpClient(adapter, HttpClientSettings(urlOf("http://host:1234/base")))

Settings

If you want to configure options for the client, you can create it with the following code:

1
2
3
4
5
6
7
8
9
// All client settings parameters are optionals and provide default values
HttpClient(adapter, HttpClientSettings(
    baseUrl = urlOf("http://host:1234/base"),
    contentType = ContentType(APPLICATION_JSON),
    useCookies = true,
    headers = Headers(Header("x-api-Key", "cafebabe")), // Headers used in all requests
    insecure = false,               // If true, the client doesn't check server certificates
    sslSettings = SslSettings()     // Key stores settings (check TLS section for details)
))

Send generic requests

The most common use case is to send a request and get a response. For details about how to use requests and responses, refer to the Request and the Response API.

Check this code snippet to get a glimpse on how to send the most general requests:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
val request = HttpRequest(
    method = GET,
    path = "/",
    body = mapOf("body" to "payload").serialize(),
    headers = Headers(Header("x-header", "value")),
    queryParameters = QueryParameters(QueryParameter("qp", "qpValue")),
    contentType = ContentType(APPLICATION_JSON)
)

val response = client.send(request)

Simple requests shortcuts

There are utility methods to make the most common request in an easy way.

Without body

1
2
3
4
5
6
7
8
val responseGet = client.get("/")
val responseHead = client.head("/")
val responsePost = client.post("/")
val responsePut = client.put("/")
val responseDelete = client.delete("/")
val responseTrace = client.trace("/")
val responseOptions = client.options("/")
val responsePatch = client.patch("/")

With body

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
val body = mapOf("key" to "value")
val serializedBody = body.serialize()

val responseGet = client.get("/", body = serializedBody)
val responsePost = client.post("/", serializedBody)
val responsePut = client.put("/", serializedBody)
val responseDelete = client.delete("/", serializedBody)
val responseTrace = client.trace("/", serializedBody)
val responseOptions = client.options("/", serializedBody)
val responsePatch = client.patch("/", serializedBody)

With body and content type

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
val body = mapOf("key" to "value")
val serializedBody = body.serialize(APPLICATION_YAML)
val yaml = ContentType(APPLICATION_YAML)

val responseGet = client.get("/", body = serializedBody, contentType = yaml)
val responsePost = client.post("/", serializedBody, contentType = yaml)
val responsePut = client.put("/", serializedBody, contentType = yaml)
val responseDelete = client.delete("/", serializedBody, contentType = yaml)
val responseTrace = client.trace("/", serializedBody, contentType = yaml)
val responseOptions = client.options("/", serializedBody, contentType = yaml)
val responsePatch = client.patch("/", serializedBody, contentType = yaml)

Cookies

The HTTP client support setting cookies from client side and updates them after any server request. Check the details in the following code fragment:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
val cookieName = "sampleCookie"
val cookieValue = "sampleCookieValue"

// Set the cookie in the client
client.cookies += Cookie(cookieName, cookieValue)

// Assert that it is received in the server and change its value afterward
client.post("/assertHasCookie?cookieName=$cookieName")
client.post("/addCookie?cookieName=$cookieName&cookieValue=${cookieValue}_changed")

// Verify that the client cookie is updated
assertEquals(cookieValue + "_changed", client.cookiesMap()[cookieName]?.value)

// The cookie is persisted along calls
client.post("/assertHasCookie?cookieName=$cookieName")
assertEquals(cookieValue + "_changed", client.cookiesMap()[cookieName]?.value)

You can also check the full test for more details.

Multipart (forms and files)

Using the HTTP client you can send MIME multipart parts to the server. You can use it to post forms or files.

Forms

1
2
val parts = listOf(HttpPart("name", "value"))
val response = client.send(HttpRequest(POST, path = "/multipart", parts = parts))

Files

1
2
3
val stream = urlOf("classpath:assets/index.html").readBytes()
val parts = listOf(HttpPart("file", stream, "index.html"))
val response = client.send(HttpRequest(POST, path = "/file", parts = parts))

WebSockets

Web Sockets connections can be opened with the ws method on the HTTP client. It creates a WS session that can be used to send data and listen to events through callbacks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
val results = mutableMapOf<Int, Set<String>>()

val ws = client.ws(
    path = "/ws/1",
    onText = { text ->
        synchronized(results) {
            results[1] = (results[1] ?: emptySet()) + text
        }
    },
    onClose = { status, reason ->
        logger.info { "Closed with: $status - $reason" }
    }
)

TLS

The HTTP client supports server certificates (to use HTTPS and HTTP/2) and also client certificates (to be able to do mutual TLS). Key stores may have the JKS format (deprecated), or the newer PKCS12 format.

To set up client/server certificates, you need to include SslSettings in your ClientSettings. In the sections below you can see how to configure these parameters.

Key Store

This store holds the identity certificate, this certificate is presented to the server by the client in the handshake for the server to authorize or deny the connection. The following code:

1
2
3
4
val keyStoreSettings = SslSettings(
    keyStore = urlOf("classpath:ssl/$identity"),
    keyStorePassword = identity.reversed()
)

Trust Store

This key store should include all the trusted certificates. Any certificate added as CA (certificate authority) makes the client trust any other certificate signed by them. However, you can also add standalone server certificates.

1
2
3
4
val trustStoreSettings = SslSettings(
    trustStore = urlOf("classpath:ssl/$trust"),
    trustStorePassword = trust.reversed()
)

Mutual TLS

If you set up the identity (service's own certificate) and the trust store (CAs and servers trusted by the client), you will achieve double ended authentication (server authenticated by the client, and client authenticated by the server). You can see a complete example below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// Key store files
val identity = "hexagontk.p12"
val trust = "trust.p12"

// Default passwords are file name reversed
val keyStorePassword = identity.reversed()
val trustStorePassword = trust.reversed()

// Key stores can be set as URIs to classpath resources (the triple slash is needed)
val keyStore = urlOf("classpath:ssl/$identity")
val trustStore = urlOf("classpath:ssl/$trust")

val sslSettings = SslSettings(
    keyStore = keyStore,
    keyStorePassword = keyStorePassword,
    trustStore = trustStore,
    trustStorePassword = trustStorePassword,
    clientAuth = true // Requires a valid certificate from the client (mutual TLS)
)

val serverSettings = HttpServerSettings(
    bindPort = 0,
    protocol = HTTPS, // You can also use HTTP2
    sslSettings = sslSettings
)

val server = HttpServer(serverAdapter(), serverSettings) {
    get("/hello") {
        // We can access the certificate used by the client from the request
        val subjectDn = request.certificate()?.subjectX500Principal?.name ?: ""
        val h = response.headers + Header("cert", subjectDn)
        ok("Hello World!", headers = h)
    }
}
server.start()

// We'll use the same certificate for the client (in a real scenario it would be different)
val clientSettings = HttpClientSettings(sslSettings = sslSettings)

// Create an HTTP client and make an HTTPS request
val client = HttpClient(clientAdapter(), clientSettings.copy(baseUrl = server.binding))
client.start()
client.get("/hello").apply {
    // Assure the certificate received (and returned) by the server is correct
    assert(headers.require("cert").string()?.startsWith("CN=hexagontk.com") ?: false)
    assertEquals("Hello World!", body)
}

Package com.hexagonkt.http.client

This package holds the classes that define the HTTP client and its configuration settings.