HTTP client content encoding plugin for compression and decompression in Ktor applications.
—
Content encoding algorithms for HTTP compression and decompression, including built-in encoders and custom encoder implementation.
interface ContentEncoder : Encoder {
/**
* Encoder identifier to use in http headers.
*/
val name: String
/**
* Provides an estimation for the compressed length based on the originalLength or return null if it's impossible.
*/
fun predictCompressedLength(contentLength: Long): Long? = null
}
interface Encoder {
/**
* Launch coroutine to encode source bytes.
*/
fun encode(
source: ByteReadChannel,
coroutineContext: CoroutineContext = EmptyCoroutineContext
): ByteReadChannel
/**
* Launch coroutine to encode source bytes.
*/
fun encode(
source: ByteWriteChannel,
coroutineContext: CoroutineContext = EmptyCoroutineContext
): ByteWriteChannel
/**
* Launch coroutine to decode source bytes.
*/
fun decode(
source: ByteReadChannel,
coroutineContext: CoroutineContext = EmptyCoroutineContext
): ByteReadChannel
}GZIP compression algorithm implementation with cross-platform support.
object GZipEncoder : ContentEncoder {
override val name: String // "gzip"
override fun encode(
source: ByteReadChannel,
coroutineContext: CoroutineContext
): ByteReadChannel
override fun encode(
source: ByteWriteChannel,
coroutineContext: CoroutineContext
): ByteWriteChannel
override fun decode(
source: ByteReadChannel,
coroutineContext: CoroutineContext
): ByteReadChannel
}Usage:
import io.ktor.util.*
install(ContentEncoding) {
gzip() // Enable GZIP compression
gzip(0.9f) // Enable with specific quality value
}
// Manual encoder usage
val compressed = GZipEncoder.encode(originalData, coroutineContext)
val decompressed = GZipEncoder.decode(compressedData, coroutineContext)Characteristics:
Content-Encoding: gzipDeflate compression algorithm implementation with cross-platform support.
object DeflateEncoder : ContentEncoder {
override val name: String // "deflate"
override fun encode(
source: ByteReadChannel,
coroutineContext: CoroutineContext
): ByteReadChannel
override fun encode(
source: ByteWriteChannel,
coroutineContext: CoroutineContext
): ByteWriteChannel
override fun decode(
source: ByteReadChannel,
coroutineContext: CoroutineContext
): ByteReadChannel
}Usage:
import io.ktor.util.*
install(ContentEncoding) {
deflate() // Enable Deflate compression
deflate(0.8f) // Enable with specific quality value
}
// Manual encoder usage
val compressed = DeflateEncoder.encode(originalData, coroutineContext)
val decompressed = DeflateEncoder.decode(compressedData, coroutineContext)Characteristics:
Content-Encoding: deflateIdentity (no-op) encoder that passes data through unchanged.
object IdentityEncoder : ContentEncoder {
override val name: String // "identity"
override fun encode(
source: ByteReadChannel,
coroutineContext: CoroutineContext
): ByteReadChannel
override fun encode(
source: ByteWriteChannel,
coroutineContext: CoroutineContext
): ByteWriteChannel
override fun decode(
source: ByteReadChannel,
coroutineContext: CoroutineContext
): ByteReadChannel
override fun predictCompressedLength(contentLength: Long): Long
}Usage:
install(ContentEncoding) {
identity() // Enable identity encoding
identity(0.1f) // Low priority fallback
}
// Manual encoder usage (passes data through unchanged)
val unchanged = IdentityEncoder.encode(data, coroutineContext)
val alsounchanged = IdentityEncoder.decode(data, coroutineContext)Characteristics:
Content-Encoding: identityclass CustomEncoder(private val algorithmName: String) : ContentEncoder {
override val name: String = algorithmName
override fun encode(
source: ByteReadChannel,
coroutineContext: CoroutineContext
): ByteReadChannel {
// Implement compression algorithm
return source // Placeholder - implement actual compression
}
override fun encode(
source: ByteWriteChannel,
coroutineContext: CoroutineContext
): ByteWriteChannel {
// Implement compression for write channel
return source // Placeholder - implement actual compression
}
override fun decode(
source: ByteReadChannel,
coroutineContext: CoroutineContext
): ByteReadChannel {
// Implement decompression algorithm
return source // Placeholder - implement actual decompression
}
override fun predictCompressedLength(contentLength: Long): Long? {
// Return estimated compressed size or null if unknown
return (contentLength * 0.6).toLong() // Example: 40% compression
}
}import kotlinx.coroutines.*
import io.ktor.utils.io.*
class AsyncCompressionEncoder : ContentEncoder {
override val name: String = "async-compress"
override fun encode(
source: ByteReadChannel,
coroutineContext: CoroutineContext
): ByteReadChannel {
return GlobalScope.produce(coroutineContext) {
val buffer = ByteArray(8192)
while (!source.isClosedForRead) {
val read = source.readAvailable(buffer)
if (read > 0) {
// Apply compression algorithm
val compressed = compressBytes(buffer, 0, read)
channel.writeFully(compressed)
}
}
}.channel
}
override fun encode(
source: ByteWriteChannel,
coroutineContext: CoroutineContext
): ByteWriteChannel {
return writer(coroutineContext) {
// Implement write channel compression
source.close()
}.channel
}
override fun decode(
source: ByteReadChannel,
coroutineContext: CoroutineContext
): ByteReadChannel {
return GlobalScope.produce(coroutineContext) {
val buffer = ByteArray(8192)
while (!source.isClosedForRead) {
val read = source.readAvailable(buffer)
if (read > 0) {
// Apply decompression algorithm
val decompressed = decompressBytes(buffer, 0, read)
channel.writeFully(decompressed)
}
}
}.channel
}
private fun compressBytes(data: ByteArray, offset: Int, length: Int): ByteArray {
// Implement compression logic
return data.copyOfRange(offset, offset + length)
}
private fun decompressBytes(data: ByteArray, offset: Int, length: Int): ByteArray {
// Implement decompression logic
return data.copyOfRange(offset, offset + length)
}
}val customEncoder = CustomEncoder("my-algorithm")
val client = HttpClient {
install(ContentEncoding) {
customEncoder(customEncoder, 0.9f)
gzip(0.8f) // Fallback to gzip
identity() // Final fallback
}
}install(ContentEncoding) {
gzip(1.0f) // Highest priority
deflate(0.8f) // Medium priority
identity(0.1f) // Lowest priority
}
// Generates: Accept-Encoding: gzip;q=1.0,deflate;q=0.8,identity;q=0.1// Server responds with: Content-Encoding: gzip, deflate
val response = client.get("/compressed-data")
// Decompression applied in reverse order:
// 1. deflate decoder applied first
// 2. gzip decoder applied second
// 3. Original content returned
val decoders = response.appliedDecoders
println(decoders) // ["deflate", "gzip"]// Request with multiple encodings
client.post("/upload") {
compress("gzip", "deflate") // Apply deflate, then gzip
setBody(data)
}
// Server processes in order: gzip decompression, then deflate decompression// JVM uses java.util.zip implementations
actual object GZipEncoder : ContentEncoder, Encoder by GZip {
actual override val name: String = "gzip"
}
actual object DeflateEncoder : ContentEncoder, Encoder by Deflate {
actual override val name: String = "deflate"
}// Native platforms use platform-specific compression libraries
expect object GZipEncoder : ContentEncoder {
override val name: String
// Platform-specific implementation
}// JavaScript uses browser or Node.js compression APIs
expect object GZipEncoder : ContentEncoder {
override val name: String
// Browser/Node.js specific implementation
}class UnsupportedContentEncodingException(encoding: String) :
IllegalStateException("Content-Encoding: $encoding unsupported.")Error Scenarios:
try {
val response = client.get("/data")
val content = response.body<String>()
} catch (e: UnsupportedContentEncodingException) {
println("Server used unsupported encoding: ${e.message}")
// Handle unknown encoding
}class InvalidEncoder : ContentEncoder {
override val name: String = "" // Invalid: empty name
}
install(ContentEncoding) {
try {
customEncoder(InvalidEncoder())
} catch (e: IllegalArgumentException) {
println("Invalid encoder name: ${e.message}")
}
}// High compression, slower
install(ContentEncoding) {
gzip(1.0f) // Prefer best compression
deflate(0.5f) // Lower priority
}
// Faster compression, lower ratio
install(ContentEncoding) {
deflate(1.0f) // Prefer faster algorithm
gzip(0.5f) // Lower priority
}class MemoryEfficientEncoder : ContentEncoder {
override val name: String = "memory-efficient"
override fun encode(
source: ByteReadChannel,
coroutineContext: CoroutineContext
): ByteReadChannel {
// Use streaming compression to minimize memory usage
return compressStream(source, coroutineContext)
}
private fun compressStream(
source: ByteReadChannel,
context: CoroutineContext
): ByteReadChannel {
// Implement streaming compression
TODO("Implement streaming compression")
}
}import io.ktor.client.*
import io.ktor.client.plugins.compression.*
import io.ktor.util.*
// Custom high-performance encoder
class FastEncoder : ContentEncoder {
override val name: String = "fast"
override fun encode(source: ByteReadChannel, coroutineContext: CoroutineContext): ByteReadChannel {
// Fast compression implementation
return source // Placeholder
}
override fun encode(source: ByteWriteChannel, coroutineContext: CoroutineContext): ByteWriteChannel {
return source // Placeholder
}
override fun decode(source: ByteReadChannel, coroutineContext: CoroutineContext): ByteReadChannel {
return source // Placeholder
}
override fun predictCompressedLength(contentLength: Long): Long = contentLength / 2
}
suspend fun main() {
val client = HttpClient {
install(ContentEncoding) {
mode = ContentEncodingConfig.Mode.All
// Custom encoder with highest priority
customEncoder(FastEncoder(), 1.0f)
// Standard encoders as fallbacks
gzip(0.9f)
deflate(0.8f)
identity(0.1f)
}
}
// Upload with custom compression
val uploadResponse = client.post("https://api.example.com/upload") {
compress("fast", "gzip")
setBody("Large data payload")
}
// Download with automatic decompression
val downloadResponse = client.get("https://api.example.com/download")
val content = downloadResponse.body<String>()
println("Applied decoders: ${downloadResponse.appliedDecoders}")
client.close()
}Install with Tessl CLI
npx tessl i tessl/maven-io-ktor--ktor-client-encoding