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

response-decompression.mddocs/

Response Decompression

Automatically decompress incoming HTTP response bodies based on Content-Encoding headers with transparent processing and multi-layer decompression support.

Response Decompression API

/**
 * List of ContentEncoder names that were used to decode response body.
 */
val HttpResponse.appliedDecoders: List<String>

Automatic Response Decompression

Basic Decompression

import io.ktor.client.*
import io.ktor.client.plugins.compression.*
import io.ktor.client.statement.*

val client = HttpClient {
    install(ContentEncoding) {
        // Default mode: DecompressResponse
        gzip()
        deflate()
        identity()
    }
}

// Automatic decompression based on Content-Encoding header
val response = client.get("https://api.example.com/data")
val content = response.body<String>()  // Automatically decompressed

// Check which decoders were applied
val usedDecoders = response.appliedDecoders
println("Applied decoders: $usedDecoders")  // e.g., ["gzip"]

Accept-Encoding Header Management

// Client automatically sets Accept-Encoding header
val response = client.get("/compressed-endpoint")

// Request includes: Accept-Encoding: gzip,deflate,identity
// Server responds with: Content-Encoding: gzip
// Response body is automatically decompressed

Multi-Layer Decompression

Decompression Order

// Server response with: Content-Encoding: gzip, deflate
val response = client.get("/multi-compressed")

// Decompression applied in reverse order:
// 1. gzip decompression applied first
// 2. deflate decompression applied second  
// 3. Original content returned

val decoders = response.appliedDecoders
println(decoders)  // ["gzip", "deflate"] - in application order

Complex Encoding Chains

// Server applies: deflate → gzip → brotli (if supported)
// Response header: Content-Encoding: deflate, gzip, br

val response = client.get("/complex-compression")
val content = response.body<String>()

// Decompression chain: br → gzip → deflate (reverse order)
val appliedDecoders = response.appliedDecoders
println("Decompression chain: $appliedDecoders")

Decompression Modes

DecompressResponse Mode (Default)

val client = HttpClient {
    install(ContentEncoding) {
        mode = ContentEncodingConfig.Mode.DecompressResponse
        gzip()
        deflate()
    }
}

// Only decompresses responses, never compresses requests
val response = client.get("/data")
val decompressed = response.body<String>()

All Mode (Bidirectional)

val client = HttpClient {
    install(ContentEncoding) {
        mode = ContentEncodingConfig.Mode.All
        gzip()
        deflate()
    }
}

// Both compresses requests AND decompresses responses
val response = client.post("/upload") {
    compress("gzip")  // Request compression available
    setBody(data)
}
val result = response.body<String>()  // Response decompression automatic

CompressRequest Mode

val client = HttpClient {
    install(ContentEncoding) {
        mode = ContentEncodingConfig.Mode.CompressRequest
        gzip()
    }
}

// Only compresses requests, does NOT decompress responses
val response = client.get("/data")
// Response is NOT automatically decompressed even if Content-Encoding header present

Response Processing Pipeline

Header Processing

// Response headers before decompression:
// Content-Encoding: gzip
// Content-Length: 1234 (compressed size)

val response = client.get("/compressed")

// Response headers after decompression:
// Content-Encoding: (removed)
// Content-Length: (removed - unknown after decompression)
// Other headers preserved

val content = response.body<String>()  // Decompressed content

Content-Length Handling

val response = client.get("/gzipped-content")

// Original compressed length (if available)
val originalLength = response.headers[HttpHeaders.ContentLength]?.toLong()

// Content-Length header removed after decompression
val finalLength = response.headers[HttpHeaders.ContentLength]  // null

// Actual decompressed content
val content = response.body<String>()
val actualLength = content.length

Error Handling

Unsupported Encodings

class UnsupportedContentEncodingException(encoding: String) : 
    IllegalStateException("Content-Encoding: $encoding unsupported.")

Handling Unknown Encodings:

try {
    val response = client.get("/unknown-encoding")
    val content = response.body<String>()
} catch (e: UnsupportedContentEncodingException) {
    println("Server used unsupported encoding: ${e.message}")
    
    // Fallback: handle raw compressed content
    val rawContent = response.body<ByteArray>()
    // Custom decompression logic here
}

Partial Encoding Support

val client = HttpClient {
    install(ContentEncoding) {
        gzip()      // Supported
        deflate()   // Supported
        // brotli not configured - will cause UnsupportedContentEncodingException
    }
}

// Server responds with: Content-Encoding: br, gzip
try {
    val response = client.get("/brotli-compressed")
    val content = response.body<String>()
} catch (e: UnsupportedContentEncodingException) {
    println("Brotli encoding not supported")
}

Decompression Errors

suspend fun robustDownload(url: String): String? {
    return try {
        val response = client.get(url)
        response.body<String>()
    } catch (e: UnsupportedContentEncodingException) {
        println("Encoding not supported: ${e.message}")
        null
    } catch (e: Exception) {
        println("Decompression failed: ${e.message}")
        null
    }
}

Advanced Response Decompression

Content Type Awareness

suspend fun downloadWithContentTypeHandling(url: String) {
    val response = client.get(url)
    val contentType = response.contentType()
    
    when (contentType?.contentType) {
        "text" -> {
            val text = response.body<String>()
            println("Decompressed text: ${text.take(100)}...")
        }
        "application/json" -> {
            val json = response.body<JsonObject>()
            println("Decompressed JSON keys: ${json.keys}")
        }
        "application/octet-stream" -> {
            val bytes = response.body<ByteArray>()
            println("Decompressed binary data: ${bytes.size} bytes")
        }
    }
    
    println("Applied decoders: ${response.appliedDecoders}")
}

Streaming Decompression

import io.ktor.utils.io.*

suspend fun streamingDecompression(url: String) {
    val response = client.get(url)
    val channel = response.body<ByteReadChannel>()
    
    // Content is decompressed as it's read from the channel
    val buffer = ByteArray(8192)
    while (!channel.isClosedForRead) {
        val read = channel.readAvailable(buffer)
        if (read > 0) {
            // Process decompressed data chunk
            processChunk(buffer, 0, read)
        }
    }
    
    println("Streaming decompression used: ${response.appliedDecoders}")
}

fun processChunk(data: ByteArray, offset: Int, length: Int) {
    // Process decompressed data chunk
    println("Processed ${length} decompressed bytes")
}

Conditional Decompression

suspend fun conditionalDecompression(url: String, autoDecompress: Boolean) {
    val client = if (autoDecompress) {
        HttpClient {
            install(ContentEncoding) {
                gzip()
                deflate()
            }
        }
    } else {
        HttpClient()  // No content encoding plugin
    }
    
    val response = client.get(url)
    
    if (autoDecompress) {
        val content = response.body<String>()  // Automatically decompressed
        println("Auto-decompressed content: ${content.take(100)}")
    } else {
        val rawContent = response.body<ByteArray>()  // Raw compressed content
        println("Raw content size: ${rawContent.size} bytes")
        
        // Manual decompression if needed
        val encoding = response.headers[HttpHeaders.ContentEncoding]
        if (encoding == "gzip") {
            val decompressed = manualGzipDecompression(rawContent)
            println("Manually decompressed: ${decompressed.take(100)}")
        }
    }
}

fun manualGzipDecompression(compressed: ByteArray): String {
    // Implement manual gzip decompression
    return "Manually decompressed content"
}

Response Validation

Decoder Chain Validation

suspend fun validateDecompression(url: String, expectedEncodings: List<String>) {
    val response = client.get(url)
    val content = response.body<String>()
    val appliedDecoders = response.appliedDecoders
    
    if (appliedDecoders == expectedEncodings) {
        println("✓ Expected decompression chain applied: $appliedDecoders")
    } else {
        println("✗ Unexpected decompression chain:")
        println("  Expected: $expectedEncodings")
        println("  Actual: $appliedDecoders")
    }
}

Content Integrity Verification

suspend fun verifyDecompressedContent(url: String, expectedChecksum: String) {
    val response = client.get(url)
    val content = response.body<ByteArray>()
    val actualChecksum = content.sha256()
    
    if (actualChecksum == expectedChecksum) {
        println("✓ Content integrity verified after decompression")
        println("  Decoders used: ${response.appliedDecoders}")
    } else {
        println("✗ Content integrity check failed")
        println("  Expected: $expectedChecksum")
        println("  Actual: $actualChecksum")
    }
}

fun ByteArray.sha256(): String {
    // Implement SHA-256 checksum calculation
    return "calculated-checksum"
}

Performance Optimization

Decoder Selection Strategy

val client = HttpClient {
    install(ContentEncoding) {
        // Optimize for decompression speed
        deflate(1.0f)   // Faster decompression
        gzip(0.8f)      // Slower but better compression
        identity(0.1f)  // Fallback
    }
}

Memory-Efficient Decompression

suspend fun memoryEfficientDownload(url: String): String {
    val response = client.get(url)
    
    // Stream-based decompression to minimize memory usage
    val channel = response.body<ByteReadChannel>()
    val result = StringBuilder()
    val buffer = ByteArray(4096)
    
    while (!channel.isClosedForRead) {
        val read = channel.readAvailable(buffer)
        if (read > 0) {
            result.append(String(buffer, 0, read))
        }
    }
    
    println("Memory-efficient decompression: ${response.appliedDecoders}")
    return result.toString()
}

Integration Examples

API Client with Response Validation

class ApiClient(private val client: HttpClient) {
    
    suspend fun fetchData(endpoint: String): ApiResponse? {
        return try {
            val response = client.get(endpoint)
            val data = response.body<ApiResponse>()
            
            // Log decompression info
            if (response.appliedDecoders.isNotEmpty()) {
                println("Response decompressed with: ${response.appliedDecoders}")
            }
            
            data
        } catch (e: UnsupportedContentEncodingException) {
            println("Unsupported encoding: ${e.message}")
            null
        }
    }
}

data class ApiResponse(val data: String, val status: String)

File Download Service

class DownloadService(private val client: HttpClient) {
    
    suspend fun downloadFile(url: String): ByteArray? {
        return try {
            val response = client.get(url)
            val content = response.body<ByteArray>()
            
            println("Downloaded ${content.size} bytes")
            if (response.appliedDecoders.isNotEmpty()) {
                println("Decompressed using: ${response.appliedDecoders}")
            }
            
            content
        } catch (e: Exception) {
            println("Download failed: ${e.message}")
            null
        }
    }
    
    suspend fun downloadText(url: String): String? {
        return try {
            val response = client.get(url)
            val text = response.body<String>()
            
            val decoders = response.appliedDecoders
            if (decoders.isNotEmpty()) {
                println("Text decompressed with: $decoders")
            }
            
            text
        } catch (e: Exception) {
            println("Text download failed: ${e.message}")
            null
        }
    }
}

Complete Example

import io.ktor.client.*
import io.ktor.client.plugins.compression.*
import io.ktor.client.request.*
import io.ktor.client.statement.*

suspend fun main() {
    val client = HttpClient {
        install(ContentEncoding) {
            mode = ContentEncodingConfig.Mode.DecompressResponse
            gzip()
            deflate()
            identity()
        }
    }
    
    // Simple automatic decompression
    val response1 = client.get("https://httpbin.org/gzip")
    val content1 = response1.body<String>()
    println("Gzip decompressed content: ${content1.take(100)}")
    println("Applied decoders: ${response1.appliedDecoders}")
    
    // Multi-layer decompression
    val response2 = client.get("https://example.com/multi-compressed")
    val content2 = response2.body<String>()
    println("Multi-layer decompressed: ${content2.take(100)}")
    println("Decoder chain: ${response2.appliedDecoders}")
    
    // Error handling for unsupported encodings
    try {
        val response3 = client.get("https://example.com/brotli-compressed")
        val content3 = response3.body<String>()
        println("Content: $content3")
    } catch (e: UnsupportedContentEncodingException) {
        println("Unsupported encoding: ${e.message}")
    }
    
    // Streaming decompression for large files
    val response4 = client.get("https://example.com/large-compressed-file")
    val channel = response4.body<ByteReadChannel>()
    var totalBytes = 0
    val buffer = ByteArray(8192)
    
    while (!channel.isClosedForRead) {
        val read = channel.readAvailable(buffer)
        if (read > 0) {
            totalBytes += read
            // Process chunk of decompressed data
        }
    }
    
    println("Streamed $totalBytes decompressed bytes")
    println("Streaming decoders: ${response4.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