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
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")
)

val server: Server = Server(adapter) {
    post("/books") {
        // Require fails if parameter does not exists
        val author = queryParameters.require("author")
        val title = queryParameters.require("title")
        val id = (books.keys.max() ?: 0) + 1
        books += id to Book(author, title)
        send(201, id)
    }

    get("/books/{id}") {
        val bookId = pathParameters.require("id").toInt()
        val book = books[bookId]
        if (book != null)
            // ok() is a shortcut to send(200)
            ok("Title: ${book.title}, Author: ${book.author}")
        else
            send(404, "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"] ?: book.author,
                title = queryParameters["title"] ?: book.title
            )

            ok("Book with id '$bookId' updated")
        }
        else {
            send(404, "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
            send(404, "Book not found")
    }

    // Matches path's requests with *any* HTTP method as a fallback (return 404 instead 405)
    any("/books/{id}") { send(405) }

    get("/books") { ok(books.keys.joinToString(" ", transform = Int::toString)) }
}

Session Example

Example showing how to use sessions. 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
val server: Server = Server(adapter) {
    path("/session") {
        get("/id") { ok(session.id ?: "null") }
        get("/access") { ok(session.lastAccessedTime?.toString() ?: "null") }
        get("/new") { ok(session.isNew()) }

        path("/inactive") {
            get { ok(session.maxInactiveInterval ?: "null") }

            put("/{time}") {
                session.maxInactiveInterval = pathParameters.require("time").toInt()
            }
        }

        get("/creation") { ok(session.creationTime ?: "null") }
        post("/invalidate") { session.invalidate() }

        path("/{key}") {
            put("/{value}") {
                session.set(pathParameters.require("key"), pathParameters.require("value"))
            }

            get { ok(session.get(pathParameters.require("key")).toString()) }
            delete { session.remove(pathParameters.require("key")) }
        }

        get {
            val attributes = session.attributes
            val attributeTexts = attributes.entries.map { it.key + " : " + it.value }

            response.headers["attributes"] = attributeTexts.joinToString(", ")
            response.headers["attribute values"] = attributes.values.joinToString(", ")
            response.headers["attribute names"] = attributes.keys.joinToString(", ")

            response.headers["creation"] = session.creationTime.toString()
            response.headers["id"] = session.id ?: ""
            response.headers["last access"] = session.lastAccessedTime.toString()

            response.status = 200
        }
    }
}

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
val server: Server = Server(adapter) {
    post("/assertNoCookies") {
        if (request.cookies.isNotEmpty())
            halt(500)
    }

    post("/addCookie") {
        val name = queryParameters["cookieName"]
        val value = queryParameters["cookieValue"]
        response.addCookie(HttpCookie(name, value))
    }

    post("/assertHasCookie") {
        val cookieName = queryParameters.require("cookieName")
        val cookieValue = request.cookies[cookieName]?.value
        if (queryParameters["cookieValue"] != cookieValue)
            halt(500)
    }

    post("/removeCookie") {
        response.removeCookie(queryParameters.require("cookieName"))
    }
}

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
class CustomException : IllegalArgumentException()

val server: Server = Server(adapter) {
    error(UnsupportedOperationException::class) {
        response.headers["error"] = it.message ?: it.javaClass.name
        send(599, "Unsupported")
    }

    error(IllegalArgumentException::class) {
        response.headers["runtimeError"] = it.message ?: it.javaClass.name
        send(598, "Runtime")
    }

    // Catching `Exception` handles any unhandled exception before (it has to be the last)
    error(Exception::class) { send(500, "Root handler") }

    // It is possible to execute a handler upon a given status code before returning
    error(588) { send(578, "588 -> 578") }

    get("/exception") { throw UnsupportedOperationException("error message") }
    get("/baseException") { throw CustomException() }
    get("/unhandledException") { error("error message") }

    get("/halt") { halt("halted") }
    get("/588") { halt(588) }
}

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
private val users: Map<String, String> = mapOf(
    "Turing" to "London",
    "Dijkstra" to "Rotterdam"
)

private val server: Server = Server(adapter) {
    before { attributes["start"] = nanoTime() }

    before("/protected/*") {
        val authorization = request.headers["Authorization"] ?: halt(401, "Unauthorized")
        val credentials = authorization.removePrefix("Basic ")
        val userPassword = String(Base64.getDecoder().decode(credentials)).split(":")

        // Parameters set in call attributes are accessible in other filters and routes
        attributes["username"] = userPassword[0]
        attributes["password"] = userPassword[1]
    }

    // All matching filters are run in order unless call is halted
    before("/protected/*") {
        if(users[attributes["username"]] != attributes["password"])
            halt(403, "Forbidden")
    }

    get("/protected/hi") { ok("Hello ${attributes["username"]}!") }

    // After filters are ran even if request was halted before
    after { response.headers["time"] = nanoTime() - attributes["start"] as Long }
}

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
private val server: Server = Server(adapter) {
    path("/static") {
        get("/files/*", Resource("assets")) // Serve `assets` resources on `/html/*`
        get("/resources/*", File(directory)) // Serve `test` folder on `/pub/*`
    }

    get("/html/*", Resource("assets")) // Serve `assets` resources on `/html/*`
    get("/pub/*", File(directory)) // Serve `test` folder on `/pub/*`
    get(Resource("public")) // Serve `public` resources folder on `/*`

    post("/multipart") { ok(request.parts.keys.joinToString(":")) }

    post("/file") {
        val part = request.parts.values.first()
        val content = part.inputStream.reader().readText()
        ok(content)
    }

    post("/form") {
        fun serializeMap(map: Map<String, List<String>>): List<String> = listOf(
            map.map { "${it.key}:${it.value.joinToString(",")}}" }.joinToString("\n")
        )

        val queryParams = serializeMap(queryParametersValues)
        val formParams = serializeMap(formParametersValues)

        response.headersValues["queryParams"] = queryParams
        response.headersValues["formParams"] = formParams
    }
}

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 server: Server = Server(adapter) {
    corsPath("/default", CorsSettings())
    corsPath("/example/org", CorsSettings("example.org"))
    corsPath("/no/credentials", CorsSettings(supportCredentials = false))
    corsPath("/only/post", CorsSettings(allowedMethods = setOf(POST)))
    corsPath("/cache", CorsSettings(preFlightMaxAge = 10))
    corsPath("/exposed/headers", CorsSettings(exposedHeaders = setOf("head")))
    corsPath("/allowed/headers", CorsSettings(allowedHeaders = setOf("head")))
}

private fun Router.corsPath(path: String, settings: CorsSettings) {
    path(path) {
        // CORS settings can change for different routes
        cors(settings)

        get("/path") { ok(request.method) }
        post("/path") { ok(request.method) }
        put("/path") { ok(request.method) }
        delete("/path") { ok(request.method) }
        get { ok(request.method) }
        post { ok(request.method) }
        put { ok(request.method) }
        delete { ok(request.method) }
    }
}

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
// Key store files
val identity = "hexagonkt.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 = URI("resource:///ssl/$identity")
val trustStore = URI("resource:///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 = ServerSettings(
    bindPort = 0,
    protocol = HTTPS, // You can also use HTTP2
    sslSettings = sslSettings
)

val server = serve(serverSettings, serverAdapter) {
    get("/hello") {
        // We can access the certificate used by the client from the request
        val subjectDn = request.certificate?.subjectDN?.name
        response.headers["cert"] = subjectDn
        ok("Hello World!")
    }
}

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

// Create a HTTP client and make a HTTPS request
val client = Client(AhcAdapter(), "https://localhost:${server.runtimePort}", clientSettings)
client.get("/hello").apply {
    logger.debug { body }
    // Assure the certificate received (and returned) by the server is correct
    assert(headers.require("cert").first().startsWith("CN=hexagonkt.com"))
    assert(body == "Hello World!")
}

Comments