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

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
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()[queryParameters.require("cookieName")]
        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>(NOT_FOUND) {
        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
    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 + Header("time", 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
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: HttpFields<Header> = parts.first().let { p ->
            val name = p.name
            val bodyString = p.bodyString()
            val size = p.size.toString()
            HttpFields(
                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 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 =
            HttpFields(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", 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 ServerBuilder.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
// 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 + 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 contextPath = URL("https://localhost:${server.runtimePort}")
val client = HttpClient(clientAdapter(), contextPath, clientSettings)
client.start()
client.get("/hello").apply {
    // Assure the certificate received (and returned) by the server is correct
    assert(headers.require("cert").startsWith("CN=hexagonkt.com"))
    assertEquals("Hello World!", body)
}