Skip to content

docs(http-client): specify request body compression and its opt-out - #81

Merged
ioannisj merged 4 commits into
mainfrom
posthog/spec-request-body-compression
Oct 30, 2026
Merged

ioannisj merged 4 commits into
mainfrom
posthog/spec-request-body-compression

Conversation

@posthog

@posthog posthog Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Problem

http-client mentions compression three times, always as an aside: "optional Content-Encoding (gzip)", "gzip compression for batched event uploads", and "timeout / compression / custom transport configuration". It never states the default, what an opt-out looks like, or what happens when compression is off or fails.

That gap became visible when apps on managed networks and Android work profiles had their gzipped bodies rewritten in transit and rejected with HTTP 400, with no way out because Content-Encoding is a reserved header. Three SDKs added a compression config for it, and posthog-js has carried a boolean for much longer:

  • posthog-android#793 — PostHogConfig.compression, PostHogCompression.GZIP (default) / NONE
  • posthog-ios#851 — PostHogCompression { gzip, none }, default .gzip
  • posthog-flutter#603 — same enum forwarded to both natives
  • posthog-js — disable_compression: boolean, default false

Changes

Adds one http-client requirement, applied and archived on this branch, and makes the logs and traces compression requirements defer to it:

  • Client SDKs gzip compressed bodies by default and expose a client-wide opt-out. Both the mobile enum and the posthog-js boolean satisfy it, and new surfaces should use the extensible enum shape. A client SDK may skip gzip when it's unavailable or the server's remote config doesn't advertise it.
  • Server SDKs keep their own defaults, encodings, and option names.
  • Opting out means an uncompressed body (plain JSON for JSON endpoints) and no gzip declaration on every endpoint the SDK would otherwise compress. A browser unload beacon may send a CORS-simple body such as base64 instead.
  • A compressed request declares gzip with Content-Encoding where the transport can set it. A CORS-limited browser transport may rely on the server detecting gzip, as posthog-js does.
  • For client SDKs the opt-out must be reachable through configuration. An SDK that reserves Content-Encoding against custom headers cannot point at that as the workaround.
  • A local gzip failure may fall back to an uncompressed body, but an SDK must not silently downgrade and resend after a server rejects a compressed request.
  • logs and traces send raw bodies when the http-client opt-out is on, each with a scenario.

Needs review

  • The compressed-endpoint set is deliberately left unspecified. Android compresses /flags; iOS says /flags was never gzipped. Neither PR treats that as a defect, so this PR records the off-state ("nothing you were compressing is compressed anymore") rather than inventing an endpoint list. Whether /flags should be gzipped at all is a real divergence someone should settle separately.
  • Allowing two option shapes is a judgment call. The alternative is a breaking rename on one side for no behavioral gain.

Validation

  • openspec validate document-request-body-compression --strict: valid.
  • openspec validate --specs --strict: 63 passed, 0 failed.
  • Canonical/delta parity and git diff --check pass.
  • Spec change only. No executable SDK conformance tests were added, and the scenarios are contracts rather than a claim that any SDK has been tested against them.

Created with PostHog Desktop

🤖 Generated with Claude Code

The http-client spec mentioned gzip only in passing, with no default,
no opt-out surface, and no failure behavior. Three mobile SDKs just
added a compression config after gzipped bodies were rewritten in
transit on managed networks, and posthog-js has long carried a boolean
disable switch.

Adds an http-client requirement covering the gzip default, both shipped
option shapes, the observable off-state, the reserved Content-Encoding
header, and the decision not to downgrade after a server rejection.
Leaves the per-endpoint compression set unspecified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Generated-By: PostHog Desktop
Task-Id: fb243d92-37c8-4181-8365-9985f279f6a8
@ioannisj ioannisj self-assigned this Oct 7, 2026
@ioannisj

ioannisj commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

🤖 An AI agent wrote this on behalf of @ioannisj.

Pushed one fix: the compression scenario over-specified Content-Encoding: gzip, which left out posthog-js browser's compression=gzip-js marker. It now talks about the SDK transport's gzip declaration instead, e8d4470.

@ioannisj
ioannisj marked this pull request as ready for review October 7, 2026 09:16
@ioannisj
ioannisj requested a review from a team as a code owner October 7, 2026 09:16

@turnipdabeets turnipdabeets left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the mobile/browser side of this reads right and matches android#793 / ios#851 / flutter#603 / posthog-js. Pulling back my earlier approval though, because of scope: http-client/spec.md:250 applies to any SDK that compresses bodies, and the scenarios are all @both, but several server SDKs ship something different today:

  • default off: posthog-python gzip=False (posthog/client.py:681), posthog-php compress_request = false (lib/QueueConsumer.php:36), posthog-go CompressionNone (config.go:217), posthog-rs capture_compression: None (src/client/mod.rs:290)
  • non-gzip codecs: python/go/rs offer zstd/deflate/br for capture v1
  • no config opt-out: posthog-elixir needs a custom api_client_module (lib/posthog/api/client.ex:5-8)

Could you scope "gzip, on by default, config opt-out" to client SDKs (tag those scenarios @client), and for server SDKs keep only what holds everywhere: no gzip Content-Encoding when not compressing, local-failure fallback allowed, no silent downgrade after a server rejection? Small nit: line 252 names only compression (enum) or disable_compression; server SDKs also ship positive booleans (compress_request, .NET EnableCompression, node disableCompression), so "a boolean switch" without pinning a name would be safer.

@ioannisj

Copy link
Copy Markdown
Contributor

🤖 An AI agent wrote this on behalf of @ioannisj.

Scoped it in 81a457e. The gzip default and the config opt-out now only bind client SDKs, and those three scenarios are @client. Server SDKs can default to off, offer other codecs and name the switch however they like, so python/php/go/rs conform as they ship today and elixir doesn't need a config opt-out.

What still applies to every SDK:

  • no gzip declaration and a plain JSON body when compression is off
  • the local-failure fallback
  • no silent downgrade after a rejection (that scenario stays @both)

Took the nit too, the boolean is now named in the SDK's own style with disable_compression as an example.

While in there I also dropped the compression=gzip-js example I added in e8d4470. posthog-js strips that param for gzip and sends no declaration at all (packages/browser/src/request.ts:571-573), so a CORS-limited browser transport is now a MAY that relies on the server detecting gzip, and the scenarios check that the body is gzip instead of looking for a declaration.

@turnipdabeets Mind taking another look?

@ioannisj

Copy link
Copy Markdown
Contributor

@turnipdabeets ping

@turnipdabeets turnipdabeets left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the client/server scoping reads right now, so approving. The inline comments are small wording fixes, and I'd take them before merging or in a quick follow-up.

Two things that don't have a line in this diff:

  • [suggestion] logs/spec.md:226-228 and traces/spec.md:694-697 still require gzip unless it's unavailable and don't mention an opt-out, but iOS .none sends /i/v1/logs as plain JSON and this requirement says it must. Could each of them defer to http-client for the opt-out? Traces also says the SDK "SHALL still signal explicitly", which doesn't match the no-declaration MAY for browsers here.
  • [nit] posthog-kmp has no compression option that I could find, even though it wraps the natives. Could the proposal's Impact section name it so it gets tracked? design.md could also mention the client/server split, and the PR body still reads as if the opt-out binds every SDK.

These come from reading the SDK source, I didn't run anything against the scenarios.

Comment thread openspec/specs/http-client/spec.md Resolved
Comment thread openspec/specs/http-client/spec.md Resolved
Comment thread openspec/specs/http-client/spec.md Resolved

@ioannisj ioannisj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 An AI agent wrote this on behalf of @ioannisj.

Took the two from the review body too, also in 41df4f4. Logs and traces now defer to the http-client opt-out with a scenario each, and traces only asks for an explicit signal where the transport can set it. posthog-kmp is named in Impact as missing an option, design.md has the client/server split, and the PR body is updated.

Comment thread openspec/specs/http-client/spec.md Resolved
Comment thread openspec/specs/http-client/spec.md Resolved
Comment thread openspec/specs/http-client/spec.md Resolved
@ioannisj
ioannisj merged commit d0f22fd into main Oct 30, 2026
10 checks passed
@ioannisj
ioannisj deleted the posthog/spec-request-body-compression branch October 30, 2026 15:18
eli-r-ph added a commit that referenced this pull request Oct 30, 2026
Keep both new http-client requirements: capture v1 transport (this
branch) and request body compression and opt-out (#81).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants