CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-ktor--ktor-client-encoding

HTTP client content encoding plugin for compression and decompression in Ktor applications.

Pending
Overview
Eval results
Files

encoders.mddocs/

Encoders

Content encoding algorithms for HTTP compression and decompression, including built-in encoders and custom encoder implementation.

ContentEncoder Interface

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
}

Built-in Encoders

GZipEncoder

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:

  • Header: Content-Encoding: gzip
  • Good compression ratio
  • Widely supported
  • Standard web compression algorithm
  • Platform-specific implementations

DeflateEncoder

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

  • Header: Content-Encoding: deflate
  • Fast compression/decompression
  • Good compression ratio
  • Less common than gzip
  • Platform-specific implementations

IdentityEncoder

Identity (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:

  • Header: Content-Encoding: identity
  • No compression applied
  • Data passed through unchanged
  • Used as fallback option
  • Perfect length prediction (returns original length)

Custom Encoder Implementation

Basic Custom Encoder

class 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
    }
}

Advanced Custom Encoder with Coroutines

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)
    }
}

Custom Encoder Registration

val customEncoder = CustomEncoder("my-algorithm")

val client = HttpClient {
    install(ContentEncoding) {
        customEncoder(customEncoder, 0.9f)
        gzip(0.8f)  // Fallback to gzip
        identity()  // Final fallback
    }
}

Encoder Selection and Priority

Quality Value Ordering

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 Response Processing

// 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"]

Multi-Layer Encoding Support

// 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

Platform-Specific Implementations

JVM Implementation

// 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 Implementation

// Native platforms use platform-specific compression libraries
expect object GZipEncoder : ContentEncoder {
    override val name: String
    // Platform-specific implementation
}

JavaScript Implementation

// JavaScript uses browser or Node.js compression APIs
expect object GZipEncoder : ContentEncoder {
    override val name: String
    // Browser/Node.js specific implementation
}

Error Handling

Unsupported Encodings

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
}

Encoder Validation

class InvalidEncoder : ContentEncoder {
    override val name: String = ""  // Invalid: empty name
}

install(ContentEncoding) {
    try {
        customEncoder(InvalidEncoder())
    } catch (e: IllegalArgumentException) {
        println("Invalid encoder name: ${e.message}")
    }
}

Performance Considerations

Compression Level vs Speed

// 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
}

Memory Usage

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")
    }
}

Complete Example

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

docs

configuration.md

encoders.md

index.md

request-compression.md

response-decompression.md

tile.json