[CHA-3071] feat: decode gzip-compressed webhook bodies#254
Closed
nijeesh-stream wants to merge 1 commit intomainfrom
Closed
[CHA-3071] feat: decode gzip-compressed webhook bodies#254nijeesh-stream wants to merge 1 commit intomainfrom
nijeesh-stream wants to merge 1 commit intomainfrom
Conversation
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>
Contributor
Author
|
Superseded by #255 — branch was renamed to match the Linear ticket ( |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 aContent-Encoding: gzipheader is added.X-Signatureis 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 whenContent-Encoding: gzip(case-insensitive, trimmed), passthrough onnull/\"\". Any other value raisesIllegalStateExceptionwith a message that names the offending encoding and points the operator back atwebhook_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. ThrowsSecurityExceptionon mismatch.App.verifyWebhookSignature(String apiSecret, byte[] body, String signature)— newbyte[]overload; the existingStringoverload now delegates to it instead of round-tripping through UTF-8. Equality check moves toMessageDigest.isEqualfor 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 usesjava.util.zip.GZIPInputStreamfrom the JDK — no new SDK dependencies.Docs
docs/webhooks/webhooks_overview/webhooks_overview.mdupdated:Content-EncodingrowverifyAndDecodeWebhookand the primitivedecompressWebhookBody+verifyWebhookSignaturecallsgunzip on;, Cloud Run, Spring Boot, ASP.NET) usually strip theContent-Encodingheader, in which case the existingApp.verifyWebhook(body, signature)keeps working unchanged.Tests
src/test/java/io/getstream/chat/java/WebhookCompressionTest.java(10 tests, noBasicTestextension so they run without API credentials):nulland\"\"br,brotli,zstd,deflate,compress,lz4— each must raiseIllegalStateExceptionwhose message contains bothunsupportedandgzipfailed to decompressbyte[]andStringHMAC overloads agree, returnfalseon bad signatureverifyAndDecodeWebhookhappy paths (gzip + passthrough),SecurityExceptionon mismatch, and the regression case where the signature was computed over the compressed bytes (must reject)../gradlew :spotlessJavaCheckclean on the touched files.Backward compatibility
App.verifyWebhook(body, signature)keeps the same signature; the only difference is the comparison is now constant-time and routed through the newbyte[]overload.Made with Cursor