HTTP client content encoding plugin for compression and decompression in Ktor applications.
—
Automatically decompress incoming HTTP response bodies based on Content-Encoding headers with transparent processing and multi-layer decompression support.
/**
* List of ContentEncoder names that were used to decode response body.
*/
val HttpResponse.appliedDecoders: List<String>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"]// 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// 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// 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")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>()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 automaticval 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 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 contentval 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.lengthclass 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
}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")
}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
}
}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}")
}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")
}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"
}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")
}
}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"
}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
}
}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()
}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)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
}
}
}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