From b1c37e53dc169269b98883a3fad0f656c3ea8317 Mon Sep 17 00:00:00 2001 From: Sbussiso Date: Mon, 24 Aug 2026 07:16:27 +0000 Subject: [PATCH] fix(webhooks): handle svix 2.0.0 Webhook.verify() returning None svix 2.0.0 is a breaking major release: Webhook.verify() now returns None (it only validates the signature; the parsed event is no longer returned). The weekly dependency refresh workflow runs 'uv lock --upgrade', which resolves svix>=1.99.1 to 2.0.0, and 39 webhook tests crashed with AttributeError: 'NoneType' object has no attribute 'get' at event.get('type') in both the Clerk and Resend handlers. Keep wh.verify() as the signature-validation call (it still raises WebhookVerificationError on a bad signature in both 1.x and 2.x), then parse the now-authentic payload with json.loads() ourselves. This is version-agnostic: identical behavior under svix 1.99.1 (verify returns the dict, which we overwrite with the same dict) and 2.0.0 (verify returns None, we parse the verified bytes). Verified: full backend suite (699 tests) passes under svix 2.0.0, including all 39 previously-failing webhook tests. ruff clean. CI run: https://github.com/SourceBox-LLC/Sentinel-Command/actions/runs/32700481827 --- backend/app/api/webhooks.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/backend/app/api/webhooks.py b/backend/app/api/webhooks.py index 76a47e7..46c3486 100644 --- a/backend/app/api/webhooks.py +++ b/backend/app/api/webhooks.py @@ -1,3 +1,4 @@ +import json import logging from datetime import UTC, datetime @@ -95,7 +96,15 @@ async def clerk_webhook(request: Request, db: Session = Depends(get_db)): try: wh = Webhook(settings.CLERK_WEBHOOK_SECRET) - event = wh.verify(payload, headers) + # svix 2.0.0 changed Webhook.verify() to return None (it only + # validates the signature now; the parsed event is no longer + # returned). Parse the verified payload ourselves so the code + # is identical under svix 1.x (verify returns the dict) and + # 2.x (verify returns None). verify() still raises + # WebhookVerificationError on a bad signature in both versions, + # so this block only runs when the payload is authentic. + wh.verify(payload, headers) + event = json.loads(payload) except WebhookVerificationError: raise HTTPException(status.HTTP_400_BAD_REQUEST, "Invalid signature") from None @@ -666,7 +675,14 @@ async def resend_webhook(request: Request, db: Session = Depends(get_db)): try: wh = Webhook(settings.RESEND_WEBHOOK_SECRET) - event = wh.verify(payload, headers) + # svix 2.0.0 changed Webhook.verify() to return None (it only + # validates the signature now; the parsed event is no longer + # returned). Parse the verified payload ourselves so the code + # is identical under svix 1.x (verify returns the dict) and + # 2.x (verify returns None). See the Clerk handler above for + # the full rationale. + wh.verify(payload, headers) + event = json.loads(payload) except WebhookVerificationError: raise HTTPException(status.HTTP_400_BAD_REQUEST, "Invalid signature") from None