Compression

The compression concern compresses content sent to the clients, if applicable. By default, gzip, Brotli and Zstandard are supported.

Kotlin
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:

Kotlin
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.

Kotlin
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.

Kotlin
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.