Skip to content

fix: compress request bodies with GZIPOutputStream instead of Okio GzipSink - #829

Closed
posthog[bot] wants to merge 2 commits into
mainfrom
posthog-self-driving/fixandroid-compress-request-bodies-with-b344c3
Closed

posthog[bot] wants to merge 2 commits into
mainfrom
posthog-self-driving/fixandroid-compress-request-bodies-with-b344c3

Conversation

@posthog

@posthog posthog Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Problem

  • Apps that resolve an Okio version older than 3.11.0 can send corrupt gzip request bodies. The server rejects /flags and /batch, so the app loses all flags, events and session recordings with no visible error.
  • Root cause: an Okio deflater bug that reuses a buffer (square/okio#1608), fixed in Okio 3.11.0.
  • GzipRequestInterceptor compresses through Okio GzipSink. The SDK does not pin Okio. It gets Okio only through OkHttp 4.12.0, so the app's build picks the version.
  • GZIP is still the default. The compression = NONE opt-out from #793 helps only after someone finds it.

Changes

  • Compress with java.util.zip.GZIPOutputStream into a byte array. The output no longer depends on the Okio version.
  • The body is still buffered, so Content-Length stays set. This replaces the old forceContentLength step.
  • The skip rules (compression, existing Content-Encoding, multipart) do not change.
Option Result
GZIPOutputStream (this PR) Works with every Okio version. No new dependency.
Pin Okio 3.11.0 or later Okio 3.11.0 is built with Kotlin 2.1.20. This can break the SDK's support for older Kotlin versions.

Note

The JVM tests resolve Okio 3.6.0, but the bug did not reproduce on the JVM. The new test is a round-trip guard: a large payload must decompress with java.util.zip.GZIPInputStream to the original bytes, and Content-Length must match the body. Support reproduced the original bug on an Android 16 emulator only.

💚 How did you test it?

  • New GzipRequestInterceptorTest: large payload round trip and Content-Length, plus content type.
  • ./gradlew :posthog:test: all tests pass, including the existing gzip assertions in PostHogApiTest.
  • ./gradlew spotlessCheck :posthog:apiCheck: pass. No public API change.
  • Not run: Android module tests and an on-device check. The sandbox has no Android SDK. The interceptor is in the core module.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

🤖 Agent context

Autonomy: Fully autonomous

  • PostHog Desktop self-driving agent (Claude Code) made this change from an inbox report.
  • Rejected option: pin Okio 3.11.0 or later, because of the Kotlin 2.1.20 build (see the table above).
  • Success signal: event volume from the affected Android devices comes back after a release with this change.
  • Short-term workaround for support: upgrade Okio to 3.11.0 or later, or set compression to NONE.

Created with PostHog Desktop from this inbox report.

🤖 Generated with Claude Code

Origin

  • Conversations: ticket
  • First signal: 2026-09-30
  • Inbox report: open
  • Likely cause: 6a53f45
  • Task started by: auto-start, after the report was rated P2 and ready to fix

…ipSink

Okio versions before 3.11.0 have a deflater bug (lysine-dev/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 <noreply@anthropic.com>

Generated-By: PostHog Desktop
Task-Id: 0d03b0a3-9ca8-495f-a56c-61411d9b0b54
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Generated-By: PostHog Desktop
Task-Id: 0d03b0a3-9ca8-495f-a56c-61411d9b0b54
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

posthog-android Compliance Report

Date: 2026-10-01 17:40:41 UTC
Duration: 118349ms

✅ All Tests Passed!

46/46 tests passed


Capture Tests

✅ 29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 365ms
Format Validation.Event Has Uuid ✅ 31ms
Format Validation.Event Has Lib Properties ✅ 30ms
Format Validation.Distinct Id Is String ✅ 26ms
Format Validation.Token Is Present ✅ 23ms
Format Validation.Custom Properties Preserved ✅ 27ms
Format Validation.Event Has Timestamp ✅ 28ms
Retry Behavior.Retries On 503 ✅ 7025ms
Retry Behavior.Does Not Retry On 400 ✅ 4026ms
Retry Behavior.Does Not Retry On 401 ✅ 4024ms
Retry Behavior.Respects Retry After Header ✅ 7027ms
Retry Behavior.Implements Backoff ✅ 17035ms
Retry Behavior.Retries On 500 ✅ 7019ms
Retry Behavior.Retries On 502 ✅ 7020ms
Retry Behavior.Retries On 504 ✅ 7019ms
Retry Behavior.Max Retries Respected ✅ 17037ms
Deduplication.Generates Unique Uuids ✅ 41ms
Deduplication.Preserves Uuid On Retry ✅ 7017ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 12027ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7017ms
Deduplication.No Duplicate Events In Batch ✅ 37ms
Deduplication.Different Events Have Different Uuids ✅ 28ms
Compression.Sends Gzip When Enabled ✅ 17ms
Batch Format.Uses Proper Batch Structure ✅ 16ms
Batch Format.Flush With No Events Sends Nothing ✅ 13ms
Batch Format.Multiple Events Batched Together ✅ 37ms
Error Handling.Does Not Retry On 403 ✅ 4021ms
Error Handling.Does Not Retry On 413 ✅ 4019ms
Error Handling.Retries On 408 ✅ 5029ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 37ms
Request Payload.Flags Request Uses V2 Query Param ✅ 24ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 21ms
Request Payload.Flags Request Omits Authorization Header ✅ 26ms
Request Payload.Token In Flags Body Matches Init ✅ 33ms
Request Payload.Groups Round Trip ✅ 22ms
Request Payload.Groups Default To Empty Object ✅ 23ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 20ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 19ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 21ms
Request Lifecycle.No Flags Request On Init Alone ✅ 9ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 23ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 39ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 25ms
Retry Behavior.Retries Flags On 502 ✅ 323ms
Retry Behavior.Retries Flags On 504 ✅ 324ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 20ms

@dustinbyrne

Copy link
Copy Markdown
Contributor

setting okio min version to 3.11.0 is the solution, we don't need to swap compression

@dustinbyrne dustinbyrne closed this Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants