Request API

Request API

To handle incoming HTTP requests, genhttp-kt uses the Request abstraction, which - besides granting access to infrastructure components such as the Server, EndPoint or ClientConnection - is intended to represent the data on the wire as it was received by the connected client.

The core entry points to handle requests are handlers and concerns. The server host is passed a handler instance (known as root handler) which is then responsible for handling all incoming requests. If needed, it can use other handlers to delegate work (see layouting as a starting point).

Kotlin
interface Request {

    val header: RequestHeader

    fun getBody(headerAccess: HeaderAccess = HeaderAccess.Retain): RequestBody?

}

Memory Views

As the underlying data is just bytes (a java.nio.ByteBuffer), the API uses memory views - small classes derived from MemoryView such as ByteString, ContentType, RequestMethod, HttpProtocol or PathSegment - that wrap a read-only view of those bytes and provide type safety and convenience methods on HTTP protocol elements. This allows the core middleware of genhttp-kt to work on the data as it was received instead of converting every protocol element into a string, while providing a high level of convenience to the API users.

Kotlin
fun isHead(request: Request) = request.header.method == RequestMethod.Head

Memory views can either be constructed from memory (which happens when the request is constructed by the framework) or from strings (which is the preferred way during initialization, e.g. as a constant at the top level of a file or in a companion object). Two memory views of the same type are equal if their bytes are, ignoring the case of ASCII letters.

Kotlin
private val CorrelationHeader = ByteString("X-Correlation-ID")

fun correlationId(request: Request): ByteString? = request.header.headers.getEntry(CorrelationHeader)

Headers, Query and Cookies

RequestHeader.headers and RequestHeader.query both implement KeyValueList, an index-based list rather than a map - the request may legitimately contain the same header multiple times, so map semantics would lose entries. Instead of indexing by key, use the extension functions to look up a value:

Kotlin
fun inspect(request: Request) {
    val accepted = request.header.headers.getEntry(KnownHeaders.Accept)

    val hasRange = request.header.headers.containsKey("Range")

    val page = request.header.query.getEntry("page")
}

KnownHeaders provides pre-allocated ByteString instances for headers used throughout the framework, avoiding a conversion on every lookup. To iterate all entries of a list (e.g. to log every header sent by the client), use count together with getStringEntry, which returns the name and the value as a Pair:

Kotlin
fun log(request: Request) {
    val headers = request.header.headers

    for (i in 0 until headers.count) {
        val (name, value) = headers.getStringEntry(i)

        println("$name: $value")
    }
}

Cookies sent by the client are parsed from the Cookie header on demand:

Kotlin
fun cookies(request: Request) {
    val session = request.header.headers.getCookie("session") // single cookie

    val all = request.header.headers.getCookies() // all cookies, as a KeyValueList
}

If your application runs behind a reverse proxy (see security), the client address and protocol seen by request.client reflect the proxy, not the original caller. Use getForwardings() to read the proxy chain from the Forwarded header (RFC 7239) or the legacy X-Forwarded-* headers. The address a request was forwarded for is forAddress, as for is a keyword in Kotlin:

Kotlin
fun originalClient(request: Request) {
    val hops = request.header.headers.getForwardings()

    val originalClient = hops.firstOrNull()?.forAddress
}

Header Retention

When reading the body of an incoming request, genhttp-kt normally keeps request.header available for the remainder of the request. To achieve this, the engine retains a copy of the header data before the connection buffer backing the original request is reused to read the body. This is the default behavior, HeaderAccess.Retain, and requires no action on your part.

For high-performance handlers that no longer need the header after starting to read the body, this copy can be avoided by explicitly releasing the header:

Kotlin
fun readBody(request: Request) {
    val body = request.getBody(HeaderAccess.Release)
}

With HeaderAccess.Release, the memory used by the header can be reused instead of being copied. Consequently, accessing request.header after the body has been read will throw an exception. Only use this option when you are certain that the header is no longer needed.

Properties

Both Request and Server expose a properties bag (PropertyBag) that lets handlers and concerns carry arbitrary state without threading it through function signatures. Values stored in request.properties live for the duration of that request; values stored in server.properties live for the duration of the server and are visible to every request it handles. tryGet returns the value if there is one of the requested type, and null otherwise:

Kotlin
data class Account(val name: String)

fun remember(request: Request, account: Account) {
    request.properties["account"] = account
}

fun recall(request: Request): Account? = request.properties.tryGet<Account>("account")

Response Generation

The handler interface expects a Response? to be generated by the web application to be served to the connected client. To do so, the request.respond() function provides a response builder that can be used to generate a response.

Similar to the request interface, this builder allows to directly write the memory of the HTTP response, with convenience functions to work with strings:

Kotlin
val handler = Handler.from { request ->
    request.respond()
        .status(ResponseStatus.NoContent)
        .header("X-My-Header", "My-Value")
        .build()
}

status(...) only accepts values of the ResponseStatus enum - arbitrary status codes or custom reason phrases are not supported. To find the entry of a numeric status code, use ResponseStatus.fromCode(...).

Cookies

To set a cookie on the response, use the cookie extension on the response builder, optionally passing CookieOptions to control maxAge (a kotlin.time.Duration), expires (a java.time.Instant), domain, path, secure, httpOnly and sameSite:

Kotlin
val handler = Handler.from { request ->
    request.respond()
        .status(ResponseStatus.NoContent)
        .cookie("session", "abc123", CookieOptions(httpOnly = true, secure = true, sameSite = SameSite.Lax))
        .build()
}

Response Immutability

The Response returned by build() is immutable. A concern that wants to adjust a response already produced by the content it wraps (e.g. to add a header) cannot mutate it in place - it calls rebuild() to get a new builder seeded with the existing status, headers and content, changes what it needs, and builds again:

Kotlin
class HandledByConcern(override val content: Handler) : Concern {

    override suspend fun prepare(server: Server) {
        content.prepare(server)
    }

    override suspend fun handle(request: Request): Response? =
        content.handle(request)
            ?.rebuild()
            ?.header("X-Handled-By", "MyConcern")
            ?.build()

}

Response Content

To write a HTTP response body, pass a ResponseContent instance to the response builder. The implementing instance will have access to the ResponseSink that allows to write binary data to the wire, either through its writer (a WritableByteChannel) or its stream (an OutputStream). The following sample shows a simplified version of StringContent from the IO module, a class to send UTF-8 encoded strings as a HTTP response:

Kotlin
import org.genhttp.api.protocol.ContentType
import org.genhttp.api.protocol.Request
import org.genhttp.api.protocol.Response
import org.genhttp.api.protocol.ResponseContent
import org.genhttp.api.protocol.ResponseSink

import java.nio.ByteBuffer

class StringContent(content: String, contentType: ContentType? = null) : ResponseContent {

    private val bytes = content.toByteArray(Charsets.UTF_8)

    private val checksum = content.hashCode().toLong()

    override val length: Long = bytes.size.toLong()

    override val type: ContentType = contentType ?: ContentType.TextPlain

    override val encoding: ByteBuffer? = null

    override suspend fun calculateChecksum(): Long = checksum

    override suspend fun write(sink: ResponseSink) {
        sink.stream.write(bytes)
    }

}

// usage in a handler or concern:
fun hello(request: Request): Response =
    request.respond()
        .content(StringContent("Hello World"))
        .build()