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:
| 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:
| // 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:
| val request = HttpRequest(
method = GET,
path = "/",
body = mapOf("body" to "payload").serialize(defaultFormat),
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
| 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
| val body = mapOf("key" to "value")
val serializedBody = body.serialize(defaultFormat)
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
| 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.
Using the HTTP client you can send MIME multipart parts to the server. You can use it to post forms
or files.
Files
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:
| 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.
| 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 = serverBase(server)))
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.