From fcc31fb08ad7639c397da09b53ea99c684ae2599 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:31:22 +0000 Subject: [PATCH 1/2] fix: compress request bodies with GZIPOutputStream instead of Okio GzipSink Okio versions before 3.11.0 have a deflater bug (square/okio#1608) that can produce corrupt gzip bodies. The SDK does not pin Okio, so apps resolve their own version. Compress with java.util.zip.GZIPOutputStream so the output no longer depends on the Okio version. The body is still buffered, so Content-Length stays set. Co-Authored-By: Claude Opus 5.5 Generated-By: PostHog Desktop Task-Id: 0d03b0a3-9ca8-495f-a56c-61411d9b0b54 --- .changeset/gzip-without-okio.md | 5 ++ .../internal/GzipRequestInterceptor.kt | 54 ++++----------- .../internal/GzipRequestInterceptorTest.kt | 68 +++++++++++++++++++ 3 files changed, 86 insertions(+), 41 deletions(-) create mode 100644 .changeset/gzip-without-okio.md create mode 100644 posthog/src/test/java/com/posthog/internal/GzipRequestInterceptorTest.kt diff --git a/.changeset/gzip-without-okio.md b/.changeset/gzip-without-okio.md new file mode 100644 index 000000000..07fdfd07a --- /dev/null +++ b/.changeset/gzip-without-okio.md @@ -0,0 +1,5 @@ +--- +"posthog": patch +--- + +Compress request bodies with `java.util.zip.GZIPOutputStream` instead of Okio `GzipSink`, so apps that resolve an Okio version older than 3.11.0 no longer send corrupt gzip bodies diff --git a/posthog/src/main/java/com/posthog/internal/GzipRequestInterceptor.kt b/posthog/src/main/java/com/posthog/internal/GzipRequestInterceptor.kt index a84cdff8d..27f36158d 100644 --- a/posthog/src/main/java/com/posthog/internal/GzipRequestInterceptor.kt +++ b/posthog/src/main/java/com/posthog/internal/GzipRequestInterceptor.kt @@ -22,15 +22,14 @@ import com.posthog.PostHogCompression import com.posthog.PostHogConfig import com.posthog.PostHogInternal import okhttp3.Interceptor -import okhttp3.MediaType import okhttp3.MultipartBody import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import okio.Buffer -import okio.BufferedSink -import okio.GzipSink -import okio.buffer +import java.io.ByteArrayOutputStream import java.io.IOException +import java.util.zip.GZIPOutputStream // https://square.github.io/okhttp/features/interceptors/ @@ -56,7 +55,7 @@ public class GzipRequestInterceptor(private val config: PostHogConfig) : Interce try { originalRequest.newBuilder() .header("Content-Encoding", "gzip") - .method(originalRequest.method, forceContentLength(gzip(body))) + .method(originalRequest.method, gzip(body)) .build() } catch (e: Throwable) { config.logger.log("Failed to gzip the request body: $e.") @@ -67,44 +66,17 @@ public class GzipRequestInterceptor(private val config: PostHogConfig) : Interce } } - private fun gzip(body: RequestBody): RequestBody { - return object : RequestBody() { - override fun contentType(): MediaType? { - return body.contentType() - } - - override fun contentLength(): Long { - return -1 // We don't know the compressed length in advance! - } - - @Throws(IOException::class) - override fun writeTo(sink: BufferedSink) { - val gzipSink = GzipSink(sink).buffer() - body.writeTo(gzipSink) - gzipSink.close() - } - } - } - - // https://github.com/square/okhttp/issues/350 + // Okio's GzipSink can emit corrupt output with Okio versions before 3.11.0 (square/okio#1608). + // Apps resolve their own Okio version, so compress with the JDK instead. + // Buffering the result also sets Content-Length, see https://github.com/square/okhttp/issues/350 @Throws(IOException::class) - private fun forceContentLength(body: RequestBody): RequestBody { - val buffer = Buffer() - body.writeTo(buffer) - - return object : RequestBody() { - override fun contentType(): MediaType? { - return body.contentType() - } + private fun gzip(body: RequestBody): RequestBody { + val uncompressed = Buffer() + body.writeTo(uncompressed) - override fun contentLength(): Long { - return buffer.size - } + val compressed = ByteArrayOutputStream() + GZIPOutputStream(compressed).use { uncompressed.writeTo(it) } - @Throws(IOException::class) - override fun writeTo(sink: BufferedSink) { - sink.write(buffer.snapshot()) - } - } + return compressed.toByteArray().toRequestBody(body.contentType()) } } diff --git a/posthog/src/test/java/com/posthog/internal/GzipRequestInterceptorTest.kt b/posthog/src/test/java/com/posthog/internal/GzipRequestInterceptorTest.kt new file mode 100644 index 000000000..40ba2c981 --- /dev/null +++ b/posthog/src/test/java/com/posthog/internal/GzipRequestInterceptorTest.kt @@ -0,0 +1,68 @@ +package com.posthog.internal + +import com.posthog.API_KEY +import com.posthog.PostHogConfig +import com.posthog.TestHttpServers +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.junit.Rule +import java.util.Random +import java.util.zip.GZIPInputStream +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class GzipRequestInterceptorTest { + @get:Rule + val httpServers = TestHttpServers() + + private fun post(payload: String): okhttp3.mockwebserver.RecordedRequest { + val http = httpServers.mockHttp() + val client = + OkHttpClient.Builder() + .addInterceptor(GzipRequestInterceptor(PostHogConfig(API_KEY))) + .build() + val request = + Request.Builder() + .url(http.url("/batch")) + .post(payload.toRequestBody("application/json; charset=utf-8".toMediaType())) + .build() + client.newCall(request).execute().close() + return http.takeRequest() + } + + private fun largePayload(): String { + val random = Random(42) + return buildString { + append("[") + repeat(20_000) { index -> + if (index > 0) append(",") + append("{\"event\":\"event_$index\",\"properties\":{\"value\":\"") + repeat(64) { append('a' + random.nextInt(26)) } + append("\"}}") + } + append("]") + } + } + + @Test + fun `compressed body of a large payload decompresses to the original payload`() { + val payload = largePayload() + + val request = post(payload) + + assertEquals("gzip", request.getHeader("Content-Encoding")) + val compressed = request.body.readByteArray() + assertEquals(compressed.size.toLong(), request.getHeader("Content-Length")?.toLong()) + val decompressed = GZIPInputStream(compressed.inputStream()).use { it.readBytes().toString(Charsets.UTF_8) } + assertEquals(payload, decompressed) + } + + @Test + fun `compressed body keeps the original content type`() { + val request = post("{}") + + assertEquals("application/json; charset=utf-8", request.getHeader("Content-Type")) + } +} From 64935dac788c6715c61c4905056ce690f95e4f80 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:32:02 +0000 Subject: [PATCH 2/2] test: import RecordedRequest in GzipRequestInterceptorTest Co-Authored-By: Claude Opus 5.5 Generated-By: PostHog Desktop Task-Id: 0d03b0a3-9ca8-495f-a56c-61411d9b0b54 --- .../java/com/posthog/internal/GzipRequestInterceptorTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/posthog/src/test/java/com/posthog/internal/GzipRequestInterceptorTest.kt b/posthog/src/test/java/com/posthog/internal/GzipRequestInterceptorTest.kt index 40ba2c981..bea099aa4 100644 --- a/posthog/src/test/java/com/posthog/internal/GzipRequestInterceptorTest.kt +++ b/posthog/src/test/java/com/posthog/internal/GzipRequestInterceptorTest.kt @@ -7,6 +7,7 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.mockwebserver.RecordedRequest import org.junit.Rule import java.util.Random import java.util.zip.GZIPInputStream @@ -17,7 +18,7 @@ internal class GzipRequestInterceptorTest { @get:Rule val httpServers = TestHttpServers() - private fun post(payload: String): okhttp3.mockwebserver.RecordedRequest { + private fun post(payload: String): RecordedRequest { val http = httpServers.mockHttp() val client = OkHttpClient.Builder()