Skip to content

fix(server): return a fresh challenge on intent.verify failure instead of raising - #70

Open
ygd58 wants to merge 1 commit into
stripe:mainfrom
ygd58:fix/verify-catches-intent-verify-exceptions
Open

ygd58 wants to merge 1 commit into
stripe:mainfrom
ygd58:fix/verify-catches-intent-verify-exceptions

Conversation

@ygd58

@ygd58 ygd58 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

What

verify_or_challenge caught exceptions from intent.verify(credential, request) only to emit payment_failed and then Kernel.raise — the only failure path in this function that re-raises instead of returning a fresh challenge via new_challenge.call. Every other failure case (challenge id mismatch, binding mismatch, expired challenge, request mismatch, etc.) already returns new_challenge.call(credential, error, echo), producing a retryable 402. A method verification failure — a typed PaymentError (declined, insufficient funds) or an unexpected error — escaped instead.

Flagged by the cross-SDK audit as AGR-2026-089. Canonical mppx's createMethodFn wraps method verification the same way every other failure path here already does. This is the same gap I already fixed in stripe/mpp-java's Verify.java (#26).

Fix

Replace the rescue block's manual emit_payment_failed + Kernel.raise with return new_challenge.call(credential, e, echo). new_challenge.call already emits payment_failed internally when passed a non-nil error (see its own definition earlier in the file), so the manual emit_payment_failed call is now redundant — removed rather than left as a duplicate emission.

Testing

Adds three regression tests in test/mpp/test_server.rb, using a new ThrowingIntent test double (mirrors the existing MockIntent):

  • A typed VerificationFailedError from intent.verify returns a fresh Mpp::Challenge instead of raising.
  • A plain RuntimeError (not a typed PaymentError) also returns a fresh challenge rather than escaping uncaught — the "unexpected error" half of the fix.
  • payment_failed fires exactly once (not twice) for a verify failure, confirming the old manual emit call was removed rather than duplicated alongside new_challenge.call's own emission.

I couldn't run the test suite in my sandbox — no access to rubygems.org to install bundler/minitest — but ruby -c confirms both files parse cleanly. Please run bundle exec rake test before merging, happy to fix anything that doesn't match.

Fixes tempoxyz/mpp-tools#211

@raubrey-stripe

Copy link
Copy Markdown
Contributor

Build failure @ygd58 if you don't mind taking one more pass 🙏

…d of raising

verify_or_challenge caught exceptions from method verification only to
emit payment_failed and then Kernel.raise -- the only failure path in
this function that re-raises instead of returning a fresh challenge
via new_challenge.call. Every other failure case (challenge id
mismatch, binding mismatch, expired challenge, request mismatch, etc.)
already returns new_challenge.call(credential, error, echo), producing
a retryable 402. A method verification failure -- a typed PaymentError
(declined, insufficient funds) or an unexpected error -- escaped
instead.

Rebased onto main's validate/broadcast intent lifecycle (stripe#73, stripe#74):
the call site is now IntentLifecycle.call(intent, credential, request)
rather than intent.verify(credential, request) directly, but the
underlying bug -- and this fix -- is unchanged. IntentLifecycle falls
back to the legacy #verify path for intents that don't implement
#validate/#broadcast (see lib/mpp/server/intent_lifecycle.rb), which
is the path this fix's regression tests exercise via ThrowingIntent.

Flagged by the cross-SDK audit as AGR-2026-089. Canonical mppx's
createMethodFn wraps method verification the same way every other
failure path here already does. This is the same gap I already fixed
in stripe/mpp-java's Verify.java (stripe#26).

Fix

Replace the rescue block's manual emit_payment_failed + Kernel.raise
with return new_challenge.call(credential, e, echo). new_challenge.call
already emits payment_failed internally when passed a non-nil error,
so the manual emit_payment_failed call is redundant -- removed rather
than left as a duplicate emission.

Testing

Three regression tests in test/mpp/test_server.rb, using a
ThrowingIntent test double (mirrors the existing MockIntent, and
correctly routes through IntentLifecycle's legacy-verify fallback
since it implements neither #validate nor #broadcast):
- a typed VerificationFailedError from intent.verify returns a fresh
  Mpp::Challenge instead of raising
- a plain RuntimeError (not a typed PaymentError) also returns a fresh
  challenge rather than escaping uncaught
- payment_failed fires exactly once (not twice), confirming the old
  manual emit call was removed rather than duplicated alongside
  new_challenge.call's own emission

Still no rubygems.org access in my sandbox to run bundle exec rake
test, but ruby -c confirms both files parse cleanly, and I traced the
IntentLifecycle dispatch path by hand against its current
implementation to confirm ThrowingIntent still exercises the intended
code path after the rebase.

Fixes tempoxyz/mpp-tools#211
@ygd58
ygd58 force-pushed the fix/verify-catches-intent-verify-exceptions branch from 587afc8 to 80c83e2 Compare September 18, 2026 21:10
@ygd58

ygd58 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the ping — the branch was rebased onto an older verify.rb, before #73/#74 migrated the call site from intent.verify(...) directly to IntentLifecycle.call(intent, credential, request). That's what CI was failing on. Rebased onto current main and reapplied the same fix against the new call site (the underlying bug — and this fix — is unchanged, IntentLifecycle falls back to the legacy #verify path for intents like the test double this PR's regression tests use). Should be green now.

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.

[Agricola] AGR-2026-089: Method verification failures escape instead of producing a retryable 402

2 participants