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:
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):
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:
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:
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:
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.