Skip to content

Commit 7402f18

Browse files
refactor(webhooks): switch to verify_and_parse_* API (CHA-3071)
Replaces the earlier verify_and_decode_webhook surface with the cross-SDK contract documented at https://getstream.io/chat/docs/node/webhooks_overview/. Module-level helpers in stream_chat.webhook: Primitives: ungzip_payload - gzip magic-byte detection + inflate decode_sqs_payload - base64 then ungzip-if-magic decode_sns_payload - alias for decode_sqs_payload verify_signature - constant-time HMAC-SHA256 comparison parse_event - JSON -> dict (typed event lands later) Composite (return parsed event dict): verify_and_parse_webhook verify_and_parse_sqs verify_and_parse_sns The composite functions auto-detect compression from body bytes, so the same handler stays correct whether or not Stream is currently compressing payloads, and behind middleware that auto-decompresses. Client instance methods (StreamChat / StreamChatAsync) mirror the three composite helpers with api_secret pulled from the client. The legacy verify_webhook(body, x_signature) -> bool boolean helper is unchanged for backward compatibility. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 7b3522f commit 7402f18

3 files changed

Lines changed: 406 additions & 381 deletions

File tree

‎stream_chat/base/client.py‎

Lines changed: 52 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -133,84 +133,66 @@ def verify_webhook(
133133
).hexdigest()
134134
return signature == x_signature
135135

136-
def decompress_webhook_body(
136+
def verify_and_parse_webhook(
137137
self,
138138
body: Union[bytes, str],
139-
content_encoding: Optional[str] = None,
140-
payload_encoding: Optional[str] = None,
141-
) -> bytes:
142-
"""Decode a (possibly compressed and/or wrapped) webhook payload.
143-
144-
Stream Chat can compress outbound webhook payloads with gzip and, for
145-
SQS / SNS firehose delivery, also wrap the compressed bytes in base64
146-
so they remain valid UTF-8 over the queue. This helper applies the
147-
encodings in order:
148-
149-
1. ``payload_encoding`` (``"base64"`` / ``"b64"``) is unwrapped first.
150-
2. ``content_encoding`` (``"gzip"``) is decompressed next.
151-
3. The raw JSON bytes are returned. The caller can ``.decode("utf-8")``
152-
or pass the value straight to :func:`json.loads`, which accepts
153-
bytes.
154-
155-
``None`` or an empty string for either encoding is a no-op, so the
156-
regular HTTP webhook path stays bytewise identical to today.
157-
158-
This method does **not** check the ``X-Signature`` header. Use
159-
:meth:`verify_and_decode_webhook` for the combined decode + verify
160-
flow.
161-
162-
:param body: raw bytes (or str) received from Stream
163-
:param content_encoding: value of the ``Content-Encoding`` header
164-
(only ``"gzip"`` is supported)
165-
:param payload_encoding: wrapper around the compressed bytes
166-
(``"base64"`` / ``"b64"``); used by the SQS / SNS firehose
167-
:returns: the uncompressed JSON body as bytes
168-
"""
169-
from stream_chat.webhook import decompress_webhook_body
170-
171-
return decompress_webhook_body(
172-
body,
173-
content_encoding=content_encoding,
174-
payload_encoding=payload_encoding,
175-
)
139+
signature: Union[str, bytes],
140+
) -> Dict[str, Any]:
141+
"""Verify and parse an HTTP webhook event.
142+
143+
Decompresses ``body`` when gzipped (detected from the body bytes),
144+
verifies the ``X-Signature`` header against the app's API secret,
145+
and returns the parsed event. The Python SDK currently returns a
146+
``dict``; typed event classes are planned for a future release.
176147
177-
def verify_and_decode_webhook(
148+
:param body: raw HTTP request body bytes Stream signed
149+
:param signature: ``X-Signature`` header value
150+
:raises stream_chat.base.exceptions.WebhookSignatureError: on
151+
signature mismatch or any decode error
152+
"""
153+
from stream_chat.webhook import verify_and_parse_webhook
154+
155+
return verify_and_parse_webhook(body, signature, self.api_secret)
156+
157+
def verify_and_parse_sqs(
178158
self,
179-
body: Union[bytes, str],
180-
x_signature: Union[str, bytes],
181-
content_encoding: Optional[str] = None,
182-
payload_encoding: Optional[str] = None,
183-
) -> bytes:
184-
"""Decode a webhook payload and verify its HMAC-SHA256 signature.
185-
186-
The signature is always computed over the **uncompressed** JSON
187-
payload, so this method first decodes the body via
188-
:meth:`decompress_webhook_body` and then compares the digest with
189-
``x_signature`` using :func:`hmac.compare_digest`.
190-
191-
Works for plain HTTP webhooks (pass the ``Content-Encoding`` header
192-
value) and for SQS / SNS firehose envelopes (additionally pass
193-
``payload_encoding="base64"``).
194-
195-
:param body: raw bytes (or str) received from Stream
196-
:param x_signature: the ``X-Signature`` header value sent by Stream
197-
:param content_encoding: value of the ``Content-Encoding`` header
198-
(only ``"gzip"`` is supported)
199-
:param payload_encoding: wrapper around the compressed bytes
200-
(``"base64"`` / ``"b64"``); used by the SQS / SNS firehose
201-
:returns: the verified, uncompressed JSON body as bytes
159+
message_body: Union[bytes, str],
160+
signature: Union[str, bytes],
161+
) -> Dict[str, Any]:
162+
"""Verify and parse an SQS firehose webhook event.
163+
164+
Reverses the base64 (+ optional gzip) wrapping on the SQS
165+
``Body``, verifies the ``X-Signature`` message attribute against
166+
the app's API secret, and returns the parsed event.
167+
168+
:param message_body: SQS message ``Body`` (string)
169+
:param signature: ``X-Signature`` message attribute value
202170
:raises stream_chat.base.exceptions.WebhookSignatureError: on
203171
signature mismatch or any decode error
204172
"""
205-
from stream_chat.webhook import verify_and_decode_webhook
173+
from stream_chat.webhook import verify_and_parse_sqs
206174

207-
return verify_and_decode_webhook(
208-
body,
209-
x_signature,
210-
api_secret=self.api_secret,
211-
content_encoding=content_encoding,
212-
payload_encoding=payload_encoding,
213-
)
175+
return verify_and_parse_sqs(message_body, signature, self.api_secret)
176+
177+
def verify_and_parse_sns(
178+
self,
179+
message: Union[bytes, str],
180+
signature: Union[str, bytes],
181+
) -> Dict[str, Any]:
182+
"""Verify and parse an SNS firehose webhook event.
183+
184+
Reverses the base64 (+ optional gzip) wrapping on the SNS
185+
``Message``, verifies the ``X-Signature`` message attribute
186+
against the app's API secret, and returns the parsed event.
187+
188+
:param message: SNS notification ``Message`` field (string)
189+
:param signature: ``X-Signature`` message attribute value
190+
:raises stream_chat.base.exceptions.WebhookSignatureError: on
191+
signature mismatch or any decode error
192+
"""
193+
from stream_chat.webhook import verify_and_parse_sns
194+
195+
return verify_and_parse_sns(message, signature, self.api_secret)
214196

215197
@abc.abstractmethod
216198
def update_app_settings(

0 commit comments

Comments
 (0)