Skip to content

[CHA-3071] feat: decode gzip-compressed webhook bodies#254

Closed
nijeesh-stream wants to merge 1 commit intomainfrom
nijeesh-stream/cha-3071-webhook-decompression
Closed

[CHA-3071] feat: decode gzip-compressed webhook bodies#254
nijeesh-stream wants to merge 1 commit intomainfrom
nijeesh-stream/cha-3071-webhook-decompression

Conversation

@nijeesh-stream
Copy link
Copy Markdown
Contributor

Ticket

Summary

Server-side webhook compression is landing in GetStream/chat#13222. When an app opts in (webhook_compression_algorithm = \"gzip\") every outbound webhook body is gzipped and a Content-Encoding: gzip header is added. X-Signature is still computed over the uncompressed JSON, so handlers must decompress before verifying.

This PR adds the customer-side primitives so handlers built on this SDK keep working when their app gets flipped on.

What's new

  • App.decompressWebhookBody(byte[] body, String contentEncoding) — gunzips when Content-Encoding: gzip (case-insensitive, trimmed), passthrough on null / \"\". Any other value raises IllegalStateException with a message that names the offending encoding and points the operator back at webhook_compression_algorithm.
  • App.verifyAndDecodeWebhook(byte[] body, String signature, String contentEncoding) (instance + static + secret-explicit forms) — chains decompression with the HMAC check and returns the raw JSON. Throws SecurityException on mismatch.
  • App.verifyWebhookSignature(String apiSecret, byte[] body, String signature) — new byte[] overload; the existing String overload now delegates to it instead of round-tripping through UTF-8. Equality check moves to MessageDigest.isEqual for constant-time comparison.

The SDK supports gzip only. Any other Content-Encoding (br, zstd, deflate, …) raises a clear error rather than silently dropping the body. gzip uses java.util.zip.GZIPInputStream from the JDK — no new SDK dependencies.

Docs

docs/webhooks/webhooks_overview/webhooks_overview.md updated:

  • header table gains a Content-Encoding row
  • new "Compressed webhook bodies" section with worked examples for both verifyAndDecodeWebhook and the primitive decompressWebhookBody + verifyWebhookSignature calls
  • explicit note that webservers / frameworks that auto-decompress request bodies (nginx gunzip on;, Cloud Run, Spring Boot, ASP.NET) usually strip the Content-Encoding header, in which case the existing App.verifyWebhook(body, signature) keeps working unchanged.

Tests

src/test/java/io/getstream/chat/java/WebhookCompressionTest.java (10 tests, no BasicTest extension so they run without API credentials):

  • gzip round-trip
  • passthrough on null and \"\"
  • case-insensitive + trimmed encoding header
  • parameterized rejection across br, brotli, zstd, deflate, compress, lz4 — each must raise IllegalStateException whose message contains both unsupported and gzip
  • corrupted gzip bytes → failed to decompress
  • byte[] and String HMAC overloads agree, return false on bad signature
  • verifyAndDecodeWebhook happy paths (gzip + passthrough), SecurityException on mismatch, and the regression case where the signature was computed over the compressed bytes (must reject).
docker run --rm -w /code -v \"\$(pwd)\":/code amazoncorretto:11 \\
  ./gradlew test --tests io.getstream.chat.java.WebhookCompressionTest
...
BUILD SUCCESSFUL
10/10 PASSED

./gradlew :spotlessJavaCheck clean on the touched files.

Backward compatibility

  • No change to existing callers. App.verifyWebhook(body, signature) keeps the same signature; the only difference is the comparison is now constant-time and routed through the new byte[] overload.
  • New methods are additive.
  • No new SDK dependencies — gzip is JDK-stdlib.

Made with Cursor

Adds App.decompressWebhookBody and App.verifyAndDecodeWebhook so
handlers can accept the new outbound webhook compression
(GetStream/chat#13222) without changing how X-Signature is verified.

decompressWebhookBody returns the body unchanged when Content-Encoding
is null or empty, gunzips with java.util.zip.GZIPInputStream when the
header is gzip (case-insensitive, trimmed), and throws
IllegalStateException for any other value with a message that points
the operator at the app's webhook_compression_algorithm setting.

verifyWebhookSignature gains a byte[] overload so the existing String
overload no longer round-trips through UTF-8 unnecessarily, and the
equality check moves to MessageDigest.isEqual so comparison is
constant-time.

verifyAndDecodeWebhook chains decompression with the HMAC check and
returns the raw JSON when the signature matches; SecurityException is
thrown otherwise. The signature is always computed over the
uncompressed bytes, matching the server.

The webhook docs are updated with the new Content-Encoding header row
and a worked example using verifyAndDecodeWebhook.

Tests cover gzip round-trip, null/empty/whitespace passthrough, case-
insensitive Content-Encoding, invalid gzip bytes, every non-gzip
encoding rejected with a clear hint, byte[] / String HMAC overload
parity, signature mismatch, and the regression case where the
signature was computed over the compressed bytes.

Co-authored-by: Cursor <cursoragent@cursor.com>
@nijeesh-stream nijeesh-stream deleted the nijeesh-stream/cha-3071-webhook-decompression branch May 6, 2026 14:40
@nijeesh-stream
Copy link
Copy Markdown
Contributor Author

Superseded by #255 — branch was renamed to match the Linear ticket (nijeeshjoshy/cha-3071-compress-webhook-payloads).

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.

1 participant