HTTP Server
Books Example
A simple CRUD example showing how to manage book resources. Here you can check the
full test.
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62 | data class Book(val author: String, val title: String)
private val books: MutableMap<Int, Book> = linkedMapOf(
100 to Book("Miguel de Cervantes", "Don Quixote"),
101 to Book("William Shakespeare", "Hamlet"),
102 to Book("Homer", "The Odyssey")
)
private val path: PathHandler = path {
post("/books") {
val author = queryParameters["author"]?.string() ?: return@post badRequest("Missing author")
val title = queryParameters["title"]?.string() ?: return@post badRequest("Missing title")
val id = (books.keys.maxOrNull() ?: 0) + 1
books += id to Book(author, title)
created(id.toString())
}
get("/books/{id}") {
val bookId = pathParameters.require("id").toInt()
val book = books[bookId]
if (book != null)
ok("Title: ${book.title}, Author: ${book.author}")
else
notFound("Book not found")
}
put("/books/{id}") {
val bookId = pathParameters.require("id").toInt()
val book = books[bookId]
if (book != null) {
books += bookId to book.copy(
author = queryParameters["author"]?.string() ?: book.author,
title = queryParameters["title"]?.string() ?: book.title
)
ok("Book with id '$bookId' updated")
}
else {
notFound("Book not found")
}
}
delete("/books/{id}") {
val bookId = pathParameters.require("id").toInt()
val book = books[bookId]
books -= bookId
if (book != null)
ok("Book with id '$bookId' deleted")
else
notFound("Book not found")
}
// Matches path's requests with *any* HTTP method as a fallback (return 405 instead 404)
after(ALL - DELETE - PUT - GET, "/books/{id}", status = NOT_FOUND_404) {
send(METHOD_NOT_ALLOWED_405)
}
get("/books") {
ok(books.keys.joinToString(" ", transform = Int::toString))
}
}
|
Cookies Example
Demo server to show the use of cookies. Here you can check the
full test.
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
48 | private val path: PathHandler = path {
post("/assertNoCookies") {
if (request.cookies.isNotEmpty()) internalServerError()
else ok()
}
post("/addCookie") {
val name = queryParameters.require("cookieName").string()
?: return@post badRequest("No cookie name")
val value = queryParameters.require("cookieValue").string()
?: return@post badRequest("No cookie value")
val maxAge = queryParameters["maxAge"]?.string()
val secure = queryParameters["secure"]?.string()
val cookiePath = queryParameters["path"]?.string()
val httpOnly = queryParameters["httpOnly"]?.string()
val domain = queryParameters["domain"]?.string()
val sameSite = queryParameters["sameSite"]?.string()
val expires = queryParameters["expires"]?.string()
ok(
cookies = response.cookies + Cookie(
name,
value,
maxAge?.toLong() ?: -1,
secure?.toBooleanStrict() ?: false,
cookiePath ?: "/",
httpOnly?.toBooleanStrict() ?: true,
domain ?: "",
sameSite?.let(::valueOf),
expires?.let(Instant::parse),
)
)
}
post("/assertHasCookie") {
val cookieName = queryParameters.require("cookieName").value ?: return@post badRequest("No cookie name")
val cookieValue = request.cookiesMap()[cookieName]?.value ?: return@post badRequest("No cookie value")
if (queryParameters["cookieValue"]?.value != cookieValue) internalServerError()
else ok()
}
post("/removeCookie") {
val cookie = request.cookiesMap()[queryParameters.require("cookieName").value]
if (cookie == null) ok()
else ok(cookies = response.cookies + cookie.delete())
}
}
|
Error Handling Example
Code to show how to handle callback exceptions and HTTP error codes. Here you can check the
full test.
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 | class CustomException : IllegalArgumentException()
private val path: PathHandler = path {
/*
* Catching `Exception` handles any unhandled exception, has to be the last executed (first
* declared)
*/
exception<Exception> {
internalServerError("Root handler")
}
exception<IllegalArgumentException> {
val error = exception?.message ?: exception?.javaClass?.name ?: fail
val newHeaders = response.headers + Header("runtime-error", error)
send(HttpStatus(598), "Runtime", headers = newHeaders)
}
exception<UnsupportedOperationException> {
val error = exception?.message ?: exception?.javaClass?.name ?: fail
val newHeaders = response.headers + Header("error", error)
send(HttpStatus(599), "Unsupported", headers = newHeaders)
}
get("/exception") { throw UnsupportedOperationException("error message") }
get("/baseException") { throw CustomException() }
get("/unhandledException") { error("error message") }
get("/invalidBody") { ok(LocalDateTime.now()) }
get("/halt") { internalServerError("halted") }
get("/588") { send(HttpStatus(588)) }
// It is possible to execute a handler upon a given status code before returning
before(pattern = "*", status = HttpStatus(588)) {
send(HttpStatus(578), "588 -> 578")
}
}
|
Filters Example
This example shows how to add filters before and after route execution. Here you can check the
full test.
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
48
49
50
51
52
53
54
55
56
57 | private val users: Map<String, String> = mapOf(
"Turing" to "London",
"Dijkstra" to "Rotterdam"
)
private val path: PathHandler = path {
filter("*") {
val start = System.nanoTime()
// Call next and store result to chain it
val next = next()
val time = (System.nanoTime() - start).toString()
// Copies result from chain with the extra data
next.send(headers = response.headers + Header("time", time))
}
filter("/protected/*") {
val authorization = request.authorization ?: return@filter unauthorized("Unauthorized")
val credentials = authorization.value
val userPassword = String(credentials.decodeBase64()).split(":")
// Parameters set in call attributes are accessible in other filters and routes
send(attributes = attributes
+ ("username" to userPassword[0])
+ ("password" to userPassword[1])
).next()
}
// All matching filters are run in order unless call is halted
filter("/protected/*") {
if(users[attributes["username"]] != attributes["password"])
send(FORBIDDEN_403, "Forbidden")
else
next()
}
get("/protected/hi") {
ok("Hello ${attributes["username"]}!")
}
path("/after") {
after(PUT) {
send(ALREADY_REPORTED_208)
}
after(PUT, "/second") {
send(NO_CONTENT_204)
}
after("/second") {
send(CREATED_201)
}
after {
send(ACCEPTED_202)
}
}
}
|
Files Example
The following code shows how to serve resources and receive files. Here you can check the
full test.
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
48
49
50
51
52
53 | private val path: PathHandler = path {
// Serve `public` resources folder on `/*`
after(
methods = setOf(GET),
pattern = "/*",
status = NOT_FOUND_404,
callback = UrlCallback(urlOf("classpath:public"))
)
path("/static") {
get("/files/*", UrlCallback(urlOf("classpath:assets")))
get("/resources/*", FileCallback(File(directory)))
}
get("/html/*", UrlCallback(urlOf("classpath:assets"))) // Serve `assets` files on `/html/*`
get("/pub/*", FileCallback(File(directory))) // Serve `test` folder on `/pub/*`
post("/multipart") {
val headers = parts.first().let { p ->
val name = p.name
val bodyString = p.bodyString()
val size = p.size.toString()
Headers(
Header("name", name),
Header("body", bodyString),
Header("size", size),
)
}
ok(headers = headers)
}
post("/file") {
val part = parts.first()
val content = part.bodyString()
val submittedFile = part.submittedFileName ?: ""
ok(content, headers = response.headers + Header("submitted-file", submittedFile))
}
post("/form") {
fun <T : HttpField> serializeMap(map: Collection<T>): List<String> = listOf(
map.joinToString("\n") { "${it.name}:${it.values.joinToString(",")}" }
)
val queryParams = serializeMap(queryParameters.values)
val formParams = serializeMap(formParameters.values)
val headers =
Headers(Header("query-params", queryParams), Header("form-params", formParams))
ok(headers = response.headers + headers)
}
}
|
CORS Example
This example shows how to set up CORS for REST APIs used from the browser. Here you can check the
full test.
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 | val path: PathHandler = path {
corsPath("/default", CorsHandler(CorsCallback()))
corsPath("/example/org", CorsHandler(allowedOrigin = "example.org"))
corsPath("/no/credentials", CorsHandler(supportCredentials = false))
corsPath("/only/post", CorsHandler(allowedMethods = setOf(POST)))
corsPath("/cache", CorsHandler(preFlightMaxAge = 10))
corsPath("/exposed/headers", CorsHandler(exposedHeaders = setOf("head")))
corsPath("/allowed/headers", CorsHandler(allowedHeaders = setOf("head")))
}
private fun HandlerBuilder.corsPath(path: String, cors: CorsHandler) {
path(path) {
// CORS settings can change for different routes
use(cors)
get("/path") { ok(method.toString()) }
post("/path") { ok(method.toString()) }
put("/path") { ok(method.toString()) }
delete("/path") { ok(method.toString()) }
get { ok(method.toString()) }
post { ok(method.toString()) }
put { ok(method.toString()) }
delete { ok(method.toString()) }
}
}
|
HTTPS Example
The snippet below shows how to set up your server to use HTTPS and HTTP/2. You can check the
full test.
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)
}
|