Compression
The compression concern compresses content sent to the clients, if applicable. By default, gzip, Brotli and Zstandard are supported.
val content = Layout.create()
.add(CompressedContent.default())This concern is part of the default configuration and will automatically
be added globally if .defaults() is used on the host.
gzip is implemented by the JDK. Brotli and Zstandard are native libraries, used through
Brotli4j and zstd-jni.
The package brings the natives of Brotli4j for Linux, Windows and macOS on x64 and arm64, zstd-jni
contains natives for these and many more platforms. To run on another platform, add the native
artifact of Brotli4j for it (com.aayushatharva.brotli4j:native-<os>-<arch>) or configure the
concern without Brotli, as shown below - otherwise, requests of clients that accept Brotli but not
Zstandard fail.
Request Analysis
The algorithm used to compress the response is negotiated with the client via the
Accept-Encoding header. If multiple algorithms are supported by the client,
the server will choose the most modern one. This is determined via the priority
of the configured compression algorithms: Zstandard first, then Brotli, then gzip.
To choose the algorithms yourself, start with an empty concern and add the ones you need:
import org.genhttp.modules.compression.CompressedContent
import org.genhttp.modules.compression.algorithms.GzipAlgorithm
import org.genhttp.modules.compression.algorithms.ZstdAlgorithm
import org.genhttp.modules.layouting.Layout
val content = Layout.create()
.add(CompressedContent.empty().add(ZstdAlgorithm()).add(GzipAlgorithm()))Response Analysis
The concern analyzes the Content-Type of the generated response to check, whether
the content can be compressed. The types enabling compression are currently fixed
and cannot be modified. Responses that are already encoded (have a Content-Encoding)
will not be changed. Compressed responses are sent with chunked transfer encoding, and
Accept-Encoding is added to their Vary header.
By default, only content larger than 256 bytes will be compressed, as compression on smaller responses
introduces more overhead than benefits. To change this limit, use .minimumSize()
on the builder. Passing null for minimal size will always compress response content.
The compression concern will tell the algorithms to use the fastest mode
available, featuring a good balance between CPU usage and resulting content size.
If you would like to adjust this setting, you can use the .level() function of the
builder.
val compression = CompressedContent.default()
.minimumSize(1024)
.level(CompressionLevel.Optimal)If the content served by your application is static files that rarely change, consider precompressing them ahead of time instead of paying the compression cost on every request.
Custom Algorithms
To add a custom compression algorithm to the server, you can implement the CompressionAlgorithm interface and register the implementing class with your server builder.
import org.genhttp.api.content.io.AlgorithmName
import org.genhttp.api.content.io.CompressionAlgorithm
import org.genhttp.api.content.io.CompressionLevel
import org.genhttp.api.infrastructure.Priority
import org.genhttp.api.protocol.ResponseContent
import org.genhttp.api.protocol.ResponseSink
import org.genhttp.engine.internal.Host
import org.genhttp.modules.compression.CompressedContent
import org.genhttp.modules.compression.compression
import org.genhttp.modules.compression.providers.CompressedResponseContent
import org.genhttp.modules.functional.Inline
import org.genhttp.modules.io.streaming.StreamBufferWriter
import org.genhttp.modules.practices.defaults
import java.io.InputStream
import java.io.OutputStream
import java.nio.channels.WritableByteChannel
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import java.util.zip.InflaterInputStream
class DeflateCompression : CompressionAlgorithm {
override val name = AlgorithmName("deflate")
override val fileExtension = "zz"
override val priority = Priority.Low
override fun compress(content: ResponseContent, level: CompressionLevel): ResponseContent =
CompressedResponseContent(content, { sink -> DeflateSink(sink, level) }, name)
override fun decompress(content: InputStream): InputStream = InflaterInputStream(content)
}
class DeflateSink(sink: ResponseSink, level: CompressionLevel) : ResponseSink, AutoCloseable {
private val deflater = Deflater(
when (level) {
CompressionLevel.Fastest -> Deflater.BEST_SPEED
CompressionLevel.Optimal -> Deflater.DEFAULT_COMPRESSION
CompressionLevel.SmallestSize -> Deflater.BEST_COMPRESSION
CompressionLevel.NoCompression -> Deflater.NO_COMPRESSION
}
)
// flushing the stream flushes the compressor, so streamed content reaches the client
override val stream: OutputStream = DeflaterOutputStream(sink.stream, deflater, true)
override val writer: WritableByteChannel by lazy { StreamBufferWriter(stream) }
// called by CompressedResponseContent when the content has been written
override fun close() {
try {
stream.close()
} finally {
deflater.end()
}
}
}
suspend fun main() {
val app = Inline.create().get { "Hello World!" }
Host.create()
.handler(app)
.defaults(compression = false)
.compression(CompressedContent.default().add(DeflateCompression()))
.run()
}CompressedResponseContent wraps the original content and passes the sink created by your
factory to it - as the length of the compressed content is not known upfront, it is sent with
chunked transfer encoding. If the sink implements AutoCloseable, it is closed after the content
has been written, which finishes the compression.