Skip to content

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"] ?: return@post badRequest("Missing author")
        val title = queryParameters["title"] ?: 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"] ?: book.author,
                title = queryParameters["title"] ?: 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 404 instead 405)
    after(ALL - DELETE - PUT - GET, "/books/{id}", status = NOT_FOUND) {
        send(METHOD_NOT_ALLOWED)
    }

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

Session Example

Example showing how to use sessions. Here you can check the full test.

TODO Add example

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
private val path: PathHandler = path {
    post("/assertNoCookies") {
        if (request.cookies.isNotEmpty()) internalServerError()
        else ok()
    }

    post("/addCookie") {
        val name = queryParameters.require("cookieName")
        val value = queryParameters.require("cookieValue")
        ok(cookies = response.cookies + HttpCookie(name, value))
    }

    post("/assertHasCookie") {
        val cookieName = queryParameters.require("cookieName")
        val cookieValue = request.cookiesMap()[cookieName]?.value
        if (queryParameters["cookieValue"] != cookieValue) internalServerError()
        else ok()
    }

    post("/removeCookie") {
        val cookie = request.cookiesMap().require(queryParameters.require("cookieName"))
        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>(NOT_FOUND) {
        internalServerError("Root handler")
    }

    exception<IllegalArgumentException> {
        val error = exception?.message ?: exception?.javaClass?.name ?: fail
        val newHeaders = response.headers + ("runtime-error" to error)
        send(HttpStatus(598), "Runtime", headers = newHeaders)
    }

    exception<UnsupportedOperationException> {
        val error = exception?.message ?: exception?.javaClass?.name ?: fail
        val newHeaders = response.headers + ("error" to 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
    on(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
58
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 + ("time" to time))
    }

    filter("/protected/*") {
        val authorization = request.headers["authorization"]
            ?: return@filter send(UNAUTHORIZED, "Unauthorized")
        val credentials = authorization.removePrefix("Basic ")
        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, "Forbidden")
        else
            next()
    }

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

    path("/after") {
        after(PUT) {
            success(ALREADY_REPORTED)
        }

        after(PUT, "/second") {
            success(NO_CONTENT)
        }

        after("/second") {
            success(CREATED)
        }

        after {
            success(ACCEPTED)
        }
    }
}

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
54
55
56
private val path: PathHandler = path {

    // Serve `public` resources folder on `/*`
    after(
        methods = setOf(GET),
        pattern = "/*",
        status = NOT_FOUND,
        callback = UrlCallback(URL("classpath:public"))
    )

    path("/static") {
        get("/files/*", UrlCallback(URL("classpath:assets")))
        get("/resources/*", FileCallback(File(directory)))
    }

    get("/html/*", UrlCallback(URL("classpath:assets"))) // Serve `assets` files on `/html/*`
    get("/pub/*", FileCallback(File(directory))) // Serve `test` folder on `/pub/*`

    post("/multipart") {
        val headers: MultiMap<String, String> = parts.first().let { p ->
            val name = p.name
            val bodyString = p.bodyString()
            val size = p.size.toString()
            val fullType = p.contentType?.mediaType?.fullType ?: ""
            val contentDisposition = p.headers.require("content-disposition")
            multiMapOf(
                "name" to name,
                "body" to bodyString,
                "size" to size,
                "type" to fullType,
                "content-disposition" to contentDisposition
            )
        }

        ok(headers = headers)
    }

    post("/file") {
        val part = parts.first()
        val content = part.bodyString()
        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(queryParameters.allValues)
        val formParams = serializeMap(formParameters.allValues)
        val headers =
            multiMapOfLists("query-params" to queryParams, "form-params" to 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", CorsCallback())
    corsPath("/example/org", CorsCallback("example.org"))
    corsPath("/no/credentials", CorsCallback(supportCredentials = false))
    corsPath("/only/post", CorsCallback(allowedMethods = setOf(POST)))
    corsPath("/cache", CorsCallback(preFlightMaxAge = 10))
    corsPath("/exposed/headers", CorsCallback(exposedHeaders = setOf("head")))
    corsPath("/allowed/headers", CorsCallback(allowedHeaders = setOf("head")))
}

private fun PathBuilder.corsPath(path: String, cors: CorsCallback) {
    path(path) {
        // CORS settings can change for different routes
        filter(pattern = "*", callback = 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
48
49
// 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 = URL("classpath:ssl/$identity")
val trustStore = URL("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 + ("cert" to 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 contextPath = URL("https://localhost:${server.runtimePort}")
val client = HttpClient(clientAdapter(), contextPath, clientSettings)
client.start()
client.get("/hello").apply {
    logger.debug { body }
    // Assure the certificate received (and returned) by the server is correct
    assert(headers.require("cert").startsWith("CN=hexagonkt.com"))
    assertEquals("Hello World!", body)
}