Skip to content

[Audit] ktor: gzip compression uses default level 6 instead of required level 1 #107

@jerrythetruckdriver

Description

@jerrythetruckdriver

Violation

File: frameworks/ktor/src/main/kotlin/com/httparena/Application.kt
Endpoint: /compression

What it does

The gzipCompress() function uses GZIPOutputStream(bos) with the default constructor:

fun gzipCompress(data: ByteArray): ByteArray {
    val bos = ByteArrayOutputStream(data.size / 4)
    GZIPOutputStream(bos).use { it.write(data) }
    return bos.toByteArray()
}

Java's GZIPOutputStream default constructor uses Deflater.DEFAULT_COMPRESSION which maps to zlib level 6.

What the spec requires

MUST use gzip level 1 (fastest)

Impact

Level 6 produces smaller output but is significantly slower than level 1. This gives ktor worse throughput on the compression benchmark than it would have at level 1, but more importantly it means the benchmark isn't measuring the same thing across frameworks.

Suggested fix

Use the GZIPOutputStream constructor that accepts a buffer size and set the deflater level explicitly:

fun gzipCompress(data: ByteArray): ByteArray {
    val bos = ByteArrayOutputStream(data.size / 4)
    val deflater = java.util.zip.Deflater(java.util.zip.Deflater.BEST_SPEED, true)
    GZIPOutputStream(bos, 8192).use { gos ->
        // Access the internal deflater via reflection or use DeflaterOutputStream directly
        it.write(data)
    }
    return bos.toByteArray()
}

Or more cleanly, use DeflaterOutputStream with gzip wrapping:

fun gzipCompress(data: ByteArray): ByteArray {
    val bos = ByteArrayOutputStream(data.size / 4)
    val deflater = java.util.zip.Deflater(java.util.zip.Deflater.BEST_SPEED, true)
    val header = byteArrayOf(0x1f.toByte(), 0x8b.toByte(), 8, 0, 0, 0, 0, 0, 0, 0)
    bos.write(header)
    java.util.zip.DeflaterOutputStream(bos, deflater).use { it.write(data) }
    // Write CRC32 and size trailer for valid gzip
    val crc = java.util.zip.CRC32()
    crc.update(data)
    // ... write trailer bytes
    return bos.toByteArray()
}

The simplest fix is probably to use GZIPOutputStream with the Deflater level set:

import java.util.zip.Deflater

fun gzipCompress(data: ByteArray): ByteArray {
    val bos = ByteArrayOutputStream(data.size / 4)
    object : GZIPOutputStream(bos) {
        init { def.setLevel(Deflater.BEST_SPEED) }
    }.use { it.write(data) }
    return bos.toByteArray()
}

Additional note

The /db endpoint uses a single shared Connection with synchronized(conn) rather than thread-local or per-worker connections as the spec requires. This serializes all concurrent DB queries through one connection, which doesn't give an unfair advantage (it hurts performance) but is technically non-compliant with the spec.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions