Skip to content

Typesafe HTTP requests with lenses

Example showing how to create and apply lenses to requests and responses to both extract and inject typesafe values out of and into HTTP messages. Note that since the http4k Request/Response objects are immutable, all injection occurs via copy.

Gradle setup

compile group: "org.http4k", name: "http4k-core", version: "3.255.0"

Standard (exception based) approach

Errors in extracting Lenses are propagated as exceptions which are caught and handled by the CatchLensFailure Filter.

package cookbook.typesafe_http_requests_with_lenses

import org.http4k.core.Body
import org.http4k.core.ContentType.Companion.TEXT_PLAIN
import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.then
import org.http4k.core.with
import org.http4k.filter.ServerFilters
import org.http4k.lens.Header
import org.http4k.lens.Query
import org.http4k.lens.int
import org.http4k.lens.string

fun main() {

    data class Child(val name: String)

    val nameHeader = Header.required("name")
    val ageQuery = Query.int().optional("age")
    val childrenBody = Body.string(TEXT_PLAIN).map({ it.split(",").map(::Child) }, { it.map { it.name }.joinToString() }).toLens()

    val endpoint = { request: Request ->

        val name: String = nameHeader(request)
        val age: Int? = ageQuery(request)
        val children: List<Child> = childrenBody(request)

        val msg = "$name is ${age ?: "unknown"} years old and has " +
            "${children.size} children (${children.map { it.name }.joinToString()})"
        Response(OK).with(
            Body.string(TEXT_PLAIN).toLens() of msg
        )
    }

    val app = ServerFilters.CatchLensFailure.then(endpoint)

    val goodRequest = Request(GET, "http://localhost:9000").with(
        nameHeader of "Jane Doe",
        ageQuery of 25,
        childrenBody of listOf(Child("Rita"), Child("Sue")))

    println(listOf("", "Request:", goodRequest, app(goodRequest)).joinToString("\n"))

    val badRequest = Request(GET, "http://localhost:9000")
        .with(nameHeader of "Jane Doe")
        .query("age", "some illegal age!")

    println(listOf("", "Request:", badRequest, app(badRequest)).joinToString("\n"))
}

Using custom "Result" ADTs

An alternative approach to using Exceptions to automatically produce BadRequests is to use an Either-type structure, and this would be easy to implement - but the lack of an in-built Result/Either type in the standard Kotlin library means that we don't have a single representation to use without shackling ourselves to another Either-containing library such as Arrow or Result4k.

Additionally, the lack of Higher Kinded Types in Kotlin means that we are unable to provide a generic method for converting standard lenses. However, it is easy to implement an extension method to use in specific use cases.

Below is an example which uses a custom Result ADT - this will work for all extraction Lenses that you define:

Code

package cookbook.typesafe_http_requests_with_lenses

import com.fasterxml.jackson.databind.JsonNode
import org.http4k.core.Body
import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.format.Jackson.json
import org.http4k.lens.LensExtractor
import org.http4k.lens.LensFailure
import org.http4k.lens.Query
import org.http4k.lens.int

// This is our custom Result/Either ADT, although it could be anything, like a Result4k Result (which has map() etc)
sealed class Result<out T>

data class Succeeded<out T>(val value: T) : Result<T>()
data class Failed<out T>(val e: Exception) : Result<T>()


// This simple extension method can be used to convert all Lenses to return our custom Result type instead of the standard exception
fun <IN, OUT> LensExtractor<IN, OUT>.toResult(): LensExtractor<IN, Result<OUT>> = object : LensExtractor<IN, Result<OUT>> {
    override fun invoke(target: IN): Result<OUT> = try {
        Succeeded(this@toResult.invoke(target))
    } catch (e: LensFailure) {
        Failed(e)
    }
}

// examples of using the above extension function
fun main() {

    val queryResultLens = Query.int().required("foo").toResult()
    val intResult: Result<Int> = queryResultLens(Request(GET, "/?foo=123"))

    println(intResult)
    val jsonResultLens = Body.json().toLens().toResult()
    val jsonResult: Result<JsonNode> = jsonResultLens(Request(GET, "/foo"))

    println(jsonResult)

}