Attach context to a request

A RequestContext makes it possible to attach objects to a request whilst it is being passed down through the layers of an application.

The basic concept is that there is a global shared object which holds a bag of state (indexed by Request). This state can be modified in Filters and then that state accessed inside other Filters or the terminating HttpHandler. There are 2 available choices for manipulating this data:

  1. Using simple Strings to represent the keys.
  2. Using RequestContextKeys and the Lens mechanism from the http4k-core module.

Whilst the first method looks technically simpler, the use of simple Strings does not provide the type-safety of the second, which uses unique shared Key objects to guarantee non-clashing of keys and type-safety of the state. Regardless of which of the above mechanisms are used, an instance of the ServerFilters.InitialiseRequestContext Filter must wrap the HttpHandler(s) to activate the shared bag of state for each request, and to remove the state after the request is complete.

Gradle setup

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

String-based keys

package guide.howto.attach_context_to_a_request

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.RequestContexts
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.then
import org.http4k.filter.ServerFilters

fun main() {
    data class SharedState(val message: String)

    fun AddState(contexts: RequestContexts) = Filter { next ->
        {
            contexts[it]["myKey"] = SharedState("hello there")
            next(it)
        }
    }

    fun PrintState(contexts: RequestContexts): HttpHandler = { request ->
        val message: SharedState? = contexts[request]["myKey"]
        println(message)
        Response(OK)
    }

    // this is the shared RequestContexts object - it holds the bag of state for each request and
    // tidies up afterwards
    val contexts = RequestContexts()

    // The first Filter is required to initialise the bag of state.
    // The second Filter modifies the bag
    // The handler just prints out the state
    val app = ServerFilters.InitialiseRequestContext(contexts)
        .then(AddState(contexts))
        .then(PrintState(contexts))

    app(Request(GET, "/hello"))
}

Lens-based keys

package guide.howto.attach_context_to_a_request

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.RequestContexts
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.RequestContextKey
import org.http4k.lens.RequestContextLens

fun main() {
    data class SharedState(val message: String)

    fun AddState(key: RequestContextLens<SharedState>) = Filter { next ->
        {
            // "modify" the request like any other Lens
            next(it.with(key of SharedState("hello there")))
        }
    }

    fun PrintState(key: RequestContextLens<SharedState>): HttpHandler = { request ->
        // we can just extract the Lens state from the request like any other standard Lens
        println(key(request))
        Response(OK)
    }

    // this is the shared RequestContexts object - it holds the bag of state for each request and
    // tidies up afterwards.
    val contexts = RequestContexts()

    // this Lens is the key we use to set and get the type-safe state. By using this, we gain
    // typesafety and the guarantee that there will be no clash of keys.
    // RequestContextKeys can be required, optional, or defaulted, as per the standard Lens mechanism.
    val key = RequestContextKey.required<SharedState>(contexts)

    // The first Filter is required to initialise the bag of state.
    // The second Filter modifies the bag.
    // The handler just prints out the state.
    val app = ServerFilters.InitialiseRequestContext(contexts)
        .then(AddState(key))
        .then(PrintState(key))

    app(Request(GET, "/hello"))
}