Core

Installation (Gradle)

dependencies {
    implementation(platform("org.http4k:http4k-bom:5.32.3.0"))
    implementation("org.http4k:http4k-core")
}

About

Apart from Kotlin StdLib, the core module has ZERO dependencies and provides the following:

  • Immutable versions of the HTTP spec objects (Request, Response, Cookies etc).
  • HTTP handler and filter abstractions which models services as simple, composable functions.
  • Simple routing implementation, plus HttpHandlerServlet to enable plugging into any Servlet engine.
  • Lens mechanism for typesafe destructuring and construction of HTTP messages.
  • Typesafe Request Context operations using Lenses.
  • Abstractions for Servers, Clients, JSON Message formats, Templating, Websockets etc.
  • SunHttp Ultra-fast single-LOC development server-backend
  • Static file-serving capability with Caching and Hot-Reload
  • Single Page App routing for React and co. See how-to guides for an example.
  • Bundled WebJars routing - activate in single-LOC. See the how-to guides for an example.
  • APIs to record and replay HTTP traffic to disk or memory

HttpHandlers

In http4k, an HTTP service is just a typealias of a simple function:

typealias HttpHandler = (Request) -> Response

First described in this Twitter paper "Your Server as a Function", this abstraction allows us lots of flexibility in a language like Kotlin, since the conceptual barrier to service construction is reduced to effectively nil. Here is the simplest example - note that we don't need any special infrastructure to create an HttpHandler, neither do we need to launch a real HTTP container to exercise it:

val handler = { request: Request -> Response(OK).body("Hello, ${request.query("name")}!") }
val get = Request(Method.GET, "/").query("name", "John Doe")
val response = handler(get)

println(response.status)
println(response.bodyString())

To mount the HttpHandler in a container, the can simply be converted to a Servlet by calling handler.asServlet()

Filters

Filters add extra processing to either the Request or Response. In http4k, they are modelled as:

interface Filter : (HttpHandler) -> HttpHandler

Filters are designed to simply compose together (using then()) , creating reusable stacks of behaviour which can then be applied to any HttpHandler. For example, to add Basic Auth and latency reporting to a service:

val handler = { _: Request -> Response(OK) }

val myFilter = Filter {
    next: HttpHandler -> {
        request: Request -> 
            val start = System.currentTimeMillis()
            val response = next(request)
            val latency = System.currentTimeMillis() - start
            println("I took $latency ms")
            response
    }
}
val latencyAndBasicAuth: Filter = ServerFilters.BasicAuth("my realm", "user", "password").then(myFilter)
val app: HttpHandler = latencyAndBasicAuth.then(handler)

The http4k-core module comes with a set of handy Filters for application to both Server and Client HttpHandlers, covering common things like:

  • Request tracing headers (x-b3-traceid etc)
  • Basic Auth
  • Cache Control
  • CORS
  • Cookie handling
  • Compression and un-compression
  • Cross-cutting concerns like logging, exception handling
  • Debugging request and responses

Check out the org.http4k.filter package for the exact list.

Testing Filters

package guide.reference.core

import com.natpryce.hamkrest.and
import com.natpryce.hamkrest.assertion.assertThat

import org.http4k.core.Filter
import org.http4k.core.HttpHandler
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.hamkrest.hasHeader
import org.http4k.hamkrest.hasStatus
import org.junit.jupiter.api.Test

val AddLatency = Filter { next ->
    {
        next(it).header("x-extra-header", "some value")
    }
}

class FilterTest {
    @Test
    fun `adds a special header`() {
        val handler: HttpHandler = AddLatency.then { Response(OK) }
        val response: Response = handler(Request(GET, "/echo/my+great+message"))
        assertThat(response, hasStatus(OK).and(hasHeader("x-extra-header", "some value")))
    }
}

Routers - Nestable, path-based Routing

Create a Router using routes() to bind a static or dynamic path to either an HttpHandler, or to another sub-Router. These Routers can be nested infinitely deep and http4k will search for a matching route using a depth-first search algorithm, before falling back finally to a 404:

routes(
    "/hello" bind routes(
        "/{name:.*}" bind GET to { request: Request -> Response(OK).body("Hello, ${request.path("name")}!") }
    ),
    "/fail" bind POST to { request: Request -> Response(INTERNAL_SERVER_ERROR) }
).asServer(Jetty(8000)).start()

Note that the http4k-contract module contains a more typesafe implementation of routing functionality, with runtime-generated live documentation in OpenApi format.

Testing Routers

package guide.testing

import com.natpryce.hamkrest.and
import com.natpryce.hamkrest.assertion.assertThat
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.hamkrest.hasBody
import org.http4k.hamkrest.hasStatus
import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.bind
import org.http4k.routing.path
import org.http4k.routing.routes
import org.junit.jupiter.api.Test

val EchoPath =
    "/echo/{message}" bind GET to { r -> Response(OK).body(r.path("message") ?: "nothing!") }

class DynamicPathTest {

    @Test
    fun `echoes body from path`() {
        val route: RoutingHttpHandler = routes(EchoPath)
        val response: Response = route(Request(GET, "/echo/my%20great%20message"))
        assertThat(response, hasStatus(OK).and(hasBody("my great message")))
    }
}

Typesafe parameter destructuring/construction of HTTP messages with Lenses

Getting values from HTTP messages is one thing, but we want to ensure that those values are both present and valid. For this purpose, we can use a Lens.

A Lens is a bi-directional entity which can be used to either get or set a particular value from/onto an HTTP message. http4k provides a DSL to configure these lenses to target particular parts of the message, whilst at the same time specifying the requirement for those parts (i.e. mandatory or optional).

To utilise a lens, first you have to declare it with the form <Location>.<configuration and mapping operations>.<terminator>.

There is one "location" type for each part of the message, each with config/mapping operations which are specific to that location:

Location Starting type Applicable to Multiplicity Requirement terminator Examples
Query String Request Singular or multiple Optional or Required Query.optional("name")
Query.required("name")
Query.int().required("name")
Query.localDate().multi.required("name")
Query.map(::CustomType, { it.value }).required("name")
Header String Request or Response Singular or multiple Optional or Required Header.optional("name")
Header.required("name")
Header.int().required("name")
Header.localDate().multi.required("name")
Header.map(::CustomType, { it.value }).required("name")
Path String Request Singular Required Path.of("name")
Path.int().of("name")
Path.map(::CustomType, { it.value }).of("name")
FormField String WebForm Singular or multiple Optional or Required FormField.optional("name")
FormField.required("name")
FormField.int().required("name")
FormField.localDate().multi.required("name")
FormField.map(::CustomType, { it.value }).required("name")
Body ByteBuffer Request or Response Singular Required Body.string(ContentType.TEXT_PLAIN).toLens()
Body.json().toLens()
Body.webForm(Validator.Strict, FormField.required("name")).toLens()

Once the lens is declared, you can use it on a target object to either get or set the value:

  • Retrieving a value: use <lens>.extract(<target>), or the more concise invoke form: <lens>(<target>)
  • Setting a value: use <lens>.inject(<value>, <target>), or the more concise invoke form: <lens>(<value>, <target>)

Code

package guide.reference.core

import org.http4k.core.Body
import org.http4k.core.ContentType
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.Path
import org.http4k.lens.Query
import org.http4k.lens.int
import org.http4k.lens.localDate
import org.http4k.lens.nonEmptyString
import org.http4k.lens.string
import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.bind
import org.http4k.routing.routes
import java.time.LocalDate

val pathLocalDate = Path.localDate().of("date")
val requiredQuery = Query.required("myQueryName")
val nonEmptyQuery = Query.nonEmptyString().required("myNonEmptyQuery")
val optionalHeader = Header.int().optional("Content-Length")
val responseBody = Body.string(ContentType.TEXT_PLAIN).toLens()

// Most of the useful common JDK types are covered. However, if we want to use our own types, we can just use `map()`
data class CustomType(val value: String)

val requiredCustomQuery = Query.map(::CustomType, { it.value }).required("myCustomType")

//To use the Lens, simply `invoke() or extract()` it using an HTTP message to extract the value, or alternatively `invoke() or inject()` it with the value if we are modifying (via copy) the message:
val handler: RoutingHttpHandler = routes(
    "/hello/{date:.*}" bind GET to { request: Request ->
        val pathDate: LocalDate = pathLocalDate(request)
        // SAME AS:
        // val pathDate: LocalDate = pathLocalDate.extract(request)

        val customType: CustomType = requiredCustomQuery(request)
        val anIntHeader: Int? = optionalHeader(request)

        val baseResponse = Response(OK)
        val responseWithHeader = optionalHeader(anIntHeader, baseResponse)
        // SAME AS:
        // val responseWithHeader = optionalHeader.inject(anIntHeader, baseResponse)

        responseBody("you sent $pathDate and $customType", responseWithHeader)
    }
)

//With the addition of the `CatchLensFailure` filter, no other validation is required when using Lenses, as http4k will handle invalid requests by returning a BAD_REQUEST (400) response.
val app = ServerFilters.CatchLensFailure.then(handler)(
    Request(
        GET,
        "/hello/2000-01-01?myCustomType=someValue"
    )
)

//More conveniently for construction of HTTP messages, multiple lenses can be used at once to modify a message, which is useful for properly building both requests and responses in a typesafe way without resorting to string values (especially in URLs which should never be constructed using String concatenation):
val modifiedRequest: Request = Request(GET, "http://google.com/{pathLocalDate}").with(
    pathLocalDate of LocalDate.now(),
    requiredQuery of "myAmazingString",
    optionalHeader of 123
)

Serving static assets

For serving static assets, just bind a path to a Static block as below, using either a Classpath or Directory (Hot reloading) based ResourceLoader instance (find these on the ResourceLoader companion object). Typically, Directory is used during development and the Classpath strategy is used to serve assets in production from an UberJar. This is usually based on a "devmode" flag when constructing your app". Note that you should avoid setting the Classpath value to the root because otherwise it will serve anything from your classpath (including Java class files!)!:

routes(
    "/static" bind static(Classpath("/org/http4k/some/package/name")),
    "/hotreload" bind static(Directory("path/to/static/dir/goes/here"))
)

Single Page Apps

These can be easily activated as below, and default to serving from /public package:

routes(
    "/reference/api" bind { Response(OK).body("some api content") },
    singlePageApp()
)

Typesafe Websockets.

Websockets have been modeled using the same methodology as standard HTTP endpoints - ie. with both simplicity and testability as a first class concern, as well as benefiting from Lens-based typesafety. Websocket communication consists of 3 main concepts:

  1. WsHandler - represented as a typealias: WsHandler = (Request) -> WsResponse. This is responsible for matching an HTTP request to a websocket.
  2. WsConsumer - represented as a typealias: WsConsumer = (WebSocket) -> Unit. This function is called on connection of a websocket and allow the API user to react to events coming from the connected websocket.
  3. WsMessage - a message which is sent or received on a websocket. This message can take advantage of the typesafety accorded to other entities in http4k by using the Lens API. Just like the http4k HTTP message model, WsMessages are immutable data classes.

The routing aspect of Websockets is done using a very similar API to the standard HTTP routing for HTTP messages and dynamic parts of the upgrade request are available when constructing a websocket instance:

 import java.nio.file.Pathdata class Wrapper(val value: String)

val body = WsMessage.string().map(::Wrapper, Wrapper::value).toLens()

val nameLens = Path.of("name")

val ws: WsHandler = websockets(
    "/hello" bind websockets(
        "/{name}" bind { req: Request ->
                WsResponse { ws: Websocket ->
                    val name = nameLens(req)
                    ws.send(WsMessage("hello $name"))
                    ws.onMessage {
                        val received = body(it)
                        ws.send(body(received))
                    }
                    ws.onClose {
                        println("closed")
                    }
                }
        }
    )
)

A WsHandler can be combined with an HttpHandler into a PolyHandler and then mounted into a supported backend server using asServer():

val app = PolyHandler(
    routes(
        "/" bind { r: Request -> Response(OK) }
    ),
    websockets(
        "/ws" bind { req: Request ->
            WsResponse { ws: Websocket ->
                ws.send(WsMessage("hello!"))
            }
        }
    )
)
app.asServer(Jetty(9000)).start()

Alternatively, the WsHandler can be also converted to a synchronous WsClient - this allows testing to be done completely offline, which allows for super-fast tests:

val client = app.testWsClient(Request(Method.GET, "ws://localhost:9000/hello/bob"))!!

client.send(WsMessage("1"))
client.close(Status(200, "bob"))

client.received.take(2).forEach(::println)

Request and Response toString()

The HttpMessages used by http4k toString in the HTTP wire format, which it simple to capture and replay HTTP message streams later in a similar way to tools like Mountebank.

CURL format

Creates curl command for a given request - this is useful to include in audit logs so exact requests can be replayed if required:

val curl = Request(POST, "http://httpbin.org/post").body(listOf("foo" to "bar").toBody()).toCurl()
// curl -X POST --data "foo=bar" "http://httpbin.org/post"