feat: Raise a distinct error for an unreadable webhook payload - #19
Merged
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y1RzepycXEYA3LStfjt8cY
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y1RzepycXEYA3LStfjt8cY
razor-x
commented
Aug 28, 2026
razor-x
commented
Aug 29, 2026
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.
What
verify()returned whatever svix handed back, cast blindlyas SeamEvent. Svix runsJSON.parseafter the signature matches (standardwebhooksreturnsJSON.parse(payload.toString())only oncetimingSafeEqualpasses), so two cases escaped the SDK's error surface:SyntaxErrorfrom inside svix.null,[1],42,"event",{}) was returned and cast toSeamEvent, so the caller got aTypeErroror silentundefinedfurther downstream.Both conflate with forgery, and the README's
catch { return res.status(400) }maps them to a signature failure — so Svix retries a permanently-unreadable payload across its full backoff schedule while operators investigate a phantom forgery.This adds
SeamInvalidWebhookPayloadError, raised only after verification succeeds:SyntaxError, retaining the original ascause.event_idandevent_typethrows the same error.WebhookVerificationErroris rethrown untouched — the webhook handler is svix, so a failed signature keeps raising svix's own error (still re-exported asSeamWebhookVerificationError).isSeamInvalidWebhookPayloadErrortype guard, exported from the index, per the convention in the other JS SDKs.An unknown/newer
event_typestill parses, so forward compatibility is unaffected.This brings the JS webhook SDK in line with the Seam SDK audit's webhook finding, already fixed in PHP (
InvalidWebhookPayloadError) and in progress for Python (seamapi/python#644).Testing
The package previously had one test (a constructor smoke test). This adds 15 more, minting real signatures with svix's own
Webhook.sign: valid event, mixed-case headers, tampered payload, wrong secret, expired timestamp, each missing svix header, signed-but-unparseable JSON, signed non-events (null,[1],42,"event",{}), and an unknown event type. Verification failures are asserted not to be payload errors, so the two classes cannot drift back together.16 tests pass;
typecheck,lint, and prettier are clean; 100% statement/function/line coverage on both touched files.🤖 Generated with Claude Code
https://claude.ai/code/session_01Y1RzepycXEYA3LStfjt8cY
Generated by Claude Code