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).
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.
fun isHead(request: Request) = request.header.method == RequestMethod.HeadMemory 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.
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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()