Response Content

When sending content in response to a client request, ResponseBuilder has a single entry point for it: content(content: ResponseContent?). Everything about the content itself - its length, type and encoding - is a property of the ResponseContent implementation being passed in, not something configured separately on the builder. length is the number of bytes the content will produce; if it is not known, chunked transfer encoding is applied.

The API provides a minimal protocol to allow the server engine to accept and consume the content to be sent to the client. It does not (and should not) know about files, streams, the classpath and so on.

To bridge the gap between the API and the real world, there is the I/O module which provides typically used types of content as well as extension functions to make it easier to work with content. Most of them accept an optional ContentType? parameter, so you rarely need to construct a ResponseContent implementation yourself just to set a type:

Kotlin
import org.genhttp.api.protocol.ContentType
import org.genhttp.api.protocol.Request
import org.genhttp.api.protocol.ResponseBuilder

import org.genhttp.modules.io.*

fun hello(request: Request): ResponseBuilder =
    request.respond()
        .content("Hello World", ContentType.TextPlain)

Content Type Guessing

When working with files that are not necessarily under your control, you might need to dynamically determine the content type of a given file. For this, you can use the guessContentType() extension provided by the I/O module, available both on String file names and on resources (where it additionally checks the resource’s own declared content type before falling back to guessing from its name):

Kotlin
import org.genhttp.api.content.io.Resource
import org.genhttp.api.protocol.ContentType

import org.genhttp.modules.io.*

fun guess(resource: Resource) {
    val type = "style.css".guessContentType() ?: ContentType.ApplicationOctetStream

    val resourceType = resource.guessContentType() // falls back to ApplicationForceDownload
}

Content Implementations

The I/O module adds StringContent, StreamContent and ResourceContent which can be used to send corresponding content to the client. Each takes the ContentType to use as a constructor argument. The following example will show you how to send strings, streams or resources as a response:

Kotlin
import org.genhttp.api.content.io.Resource
import org.genhttp.api.protocol.ContentType
import org.genhttp.api.protocol.Request
import org.genhttp.api.protocol.ResponseBuilder
import org.genhttp.engine.internal.Host

import org.genhttp.modules.io.*
import org.genhttp.modules.io.streaming.ResourceContent
import org.genhttp.modules.io.streaming.StreamContent
import org.genhttp.modules.io.streaming.calculateChecksum
import org.genhttp.modules.io.strings.StringContent
import org.genhttp.modules.layouting.*
import org.genhttp.modules.practices.*
import org.genhttp.modules.webservices.*

import java.io.ByteArrayInputStream

suspend fun main() {
    val app = Layout.create()
        .addService<ContentExamples>("content")

    Host.create()
        .handler(app)
        .defaults()
        .run()
}

class ContentExamples {

    @ResourceMethod(path = "get-string")
    fun getString(request: Request): ResponseBuilder =
        request.respond()
            .content(StringContent("This is a string", ContentType.TextPlain))

    @ResourceMethod(path = "get-resource")
    fun getResource(request: Request): ResponseBuilder {
        val resource = Resource.fromString("This is a string") // or from any other source
            .build()

        return request.respond()
            .content(ResourceContent(resource, ContentType.TextPlain))
    }

    @ResourceMethod(path = "get-stream")
    fun getStream(request: Request): ResponseBuilder {
        val data = "This is a string".toByteArray()

        val stream = ByteArrayInputStream(data)

        return request.respond()
            .content(StreamContent(stream, ContentType.TextPlain, data.size.toLong(), null, stream::calculateChecksum))
    }

}

To simplify their usage, the module also adds extensions directly on ResponseBuilder, which are recommended to be used if possible:

Kotlin
import org.genhttp.api.content.io.Resource
import org.genhttp.api.protocol.ContentType
import org.genhttp.api.protocol.Request
import org.genhttp.api.protocol.ResponseBuilder
import org.genhttp.engine.internal.Host

import org.genhttp.modules.io.*
import org.genhttp.modules.io.streaming.calculateChecksum
import org.genhttp.modules.layouting.*
import org.genhttp.modules.practices.*
import org.genhttp.modules.webservices.*

import java.io.ByteArrayInputStream

suspend fun main() {
    val app = Layout.create()
        .addService<ContentExamples>("content")

    Host.create()
        .handler(app)
        .defaults()
        .run()
}

class ContentExamples {

    @ResourceMethod(path = "get-string")
    fun getString(request: Request): ResponseBuilder =
        request.respond()
            .content("This is a string")

    @ResourceMethod(path = "get-resource")
    fun getResource(request: Request): ResponseBuilder {
        val resource = Resource.fromString("This is a string") // or from any other source
            .build()

        return request.respond()
            .content(resource)
    }

    @ResourceMethod(path = "get-stream")
    fun getStream(request: Request): ResponseBuilder {
        val data = "This is a string".toByteArray()

        val stream = ByteArrayInputStream(data)

        return request.respond()
            .content(stream, ContentType.TextPlain, data.size.toLong(), checksumProvider = stream::calculateChecksum)
    }

}

The engine closes content that is AutoCloseable - such as StreamContent and the stream it wraps - after it has been written. Calculating the checksum of a stream requires it to support mark and reset, so it can be read twice; for other streams, pass a checksumProvider or leave the checksum out.

Custom Content Implementations

The following code shows an example on how we can implement ResponseContent to efficiently serve the data stored in an entity record via content(). Besides length, an implementation needs to expose its own type and encoding and write itself to the ResponseSink passed to write:

Kotlin
import org.genhttp.api.content.Handler
import org.genhttp.api.infrastructure.Server
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 org.genhttp.api.protocol.getEntry
import org.genhttp.engine.internal.Host

import org.genhttp.modules.practices.*

import java.nio.ByteBuffer
import java.time.Instant

suspend fun main() {
    Host.create()
        .handler(AttachmentContentHandler())
        .defaults()
        .run()
}

class Attachment(val id: Int, val size: Long, val modified: Instant, val data: ByteArray)

class AttachmentContent(private val attachment: Attachment) : ResponseContent {

    override val length: Long = attachment.size

    override val type: ContentType = ContentType.ApplicationForceDownload

    override val encoding: ByteBuffer? = null

    override suspend fun calculateChecksum(): Long = attachment.modified.toEpochMilli()

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

}

class AttachmentContentHandler : Handler {

    override suspend fun prepare(server: Server) {
        // nothing to prepare
    }

    override suspend fun handle(request: Request): Response? {
        val id = request.header.query.getEntry("id")?.toIntOrNull() ?: return null

        // load the entity from some DB
        val entity = findAttachment(id) ?: return null

        return request.respond()
            .content(AttachmentContent(entity))
            .build()
    }

    private fun findAttachment(id: Int): Attachment? = TODO("load the attachment from your database")

}

While this is more complex than simply returning an InputStream from a web service, it is way more efficient for caching as we can use the modification date of the entity to check for changes.